From c8ccf188c59b9ec240a8e7e2aba618e34a639616 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Fri, 1 Oct 2021 07:32:28 -0700 Subject: [PATCH 01/29] fix Signed-off-by: Shaanjot Gill --- .../present_proof/dif/pres_exch_handler.py | 27 +++++++- .../dif/tests/test_pres_exch_handler.py | 66 +++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) 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 be33ca5c60..88115a03a4 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -1354,8 +1354,11 @@ async def apply_constraint_received_cred( split_field_path = field_path.split(".") key = ".".join(split_field_path[:-1]) value = split_field_path[-1] + additional_attrs = self.get_dict_keys_from_path( + cred_dict, key.split(".") + ) nested_field_paths = self.build_nested_paths_dict( - key, value, nested_field_paths + key, value, nested_field_paths, additional_attrs ) to_remove_from_field_paths.add(field_path) for to_remove_path in to_remove_from_field_paths: @@ -1375,6 +1378,19 @@ async def apply_constraint_received_cred( return False return True + def get_dict_keys_from_path(self, derived_cred_dict: dict, path: str) -> list: + """Return list of keys as additional_attrs to build nested_field_paths.""" + if path: + return self.get_dict_keys_from_path(derived_cred_dict[path[0]], path[1:]) + else: + additional_attrs = [] + mandatory_paths = ["@id", "id", "type"] + keys = derived_cred_dict.keys() + for key in keys: + if key in mandatory_paths: + additional_attrs.append(key) + return additional_attrs + def nested_get(self, input_dict: dict, nested_key: Sequence[str]) -> dict: """Return internal dict from nested input_dict given list of nested_key.""" internal_dict_value = input_dict @@ -1383,13 +1399,20 @@ def nested_get(self, input_dict: dict, nested_key: Sequence[str]) -> dict: return internal_dict_value def build_nested_paths_dict( - self, key: str, value: str, nested_field_paths: dict + self, + key: str, + value: str, + nested_field_paths: dict, + additional_attrs: list = None, ) -> dict: """Build and return nested_field_paths dict.""" if key in nested_field_paths.keys(): nested_field_paths[key].add(value) else: nested_field_paths[key] = {value} + if additional_attrs and len(additional_attrs) > 0: + for attr in additional_attrs: + nested_field_paths[key].add(attr) split_key = key.split(".") if len(split_key) > 1: nested_field_paths.update( 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 27dff27c32..bd86a1c804 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 @@ -3149,3 +3149,69 @@ async def test_is_holder_missing_subject(self, profile, setup_tuple): ) assert len(tmp_vp.get("verifiableCredential")) == 0 assert not tmp_vp.get("proof") + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_apply_constraint_received_cred(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_dict = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/bbs/v1", + { + "MedicalPass": { + "@id": "https://www.vdel.com/MedicalPass", + "@context": { + "description": "http://schema.org/description", + "identifier": "http://schema.org/identifier", + "name": "http://schema.org/name", + "image": "http://schema.org/image", + }, + } + }, + { + "Patient": { + "@id": "http://hl7.org/fhir/Patient", + "@context": [ + "https://fhircat.org/fhir-r5/rdf-r5/contexts/patient.context.jsonld" + ], + } + }, + ], + "id": "urn:bnid:_:c14n14", + "type": ["MedicalPass", "VerifiableCredential"], + "credentialSubject": { + "id": "urn:bnid:_:c14n11", + "Patient": { + "@id": "urn:bnid:_:c14n2", + "type": "fhir:resource-types#Patient", + "birthDate": "1958-12-03T00:00:00", + }, + }, + "issuanceDate": "2021-09-27T12:40:03+02:00", + "issuer": "did:key:zUC7DVPRfshooBqmnT2LrMxabCUkRhyyUCu8xKvYRot5aeTLTpPxzZoMyFkMLgKHMPUzdEnJM1EqbxfQd466ed3QuEtUJr8iqKRVfJ4txBa3PRoASaup6fjVAkU9VdbDbs5et64", + "proof": { + "type": "BbsBlsSignatureProof2020", + "nonce": "XVdQwHnUYJkkMX4LDWFPVuB7NQJ5IVn6ohW/psGv3fFSJ9kbr59BcWpix7Q3LBfzJ80=", + "proofValue": "ADgAAAAQAnwED6993W+P6/Iptfdk1uEaOcKDEYYJVjR154bB9r+skTZFR/P82oKJpMzZtMD+jRLV1Y4STy/JCg2nuKQVkyNbBTOxnJ5gNrYH7ZVC0IkQbq16Ld9bPkS7JeU2pfQ1fDZAL6CrkX+k7E24FFFe+ssLrkKLTDWe/J94+MJW/3qGAIuOKQ3t739ILPkElMHdNaxLNAAAAHSKc0EckEFyjM7bBqlxaLJhxkU2kcyR5ZwBee9UGzD+2s3J90rDvg4KUw7d4eli2SAAAAACGXrTBdx3eL+ki5OUBqpQ3k9oYMIwifYJdhtE4BfdMhgVwTWuNcXgXXvV46vOB1uJLi2/zaWzhAWDY9EGRM0nhot3dail73ts2KYPa+/pUMnWlmF5tVYzJNIQcPiIFzO27wA1mnFy7VXmbgHpGUmwqgAAAC5uHM39yBidgmkXY+zhOQJSC8CAJ1R2VLlOg+N8eOecIGSqaQGE7zTKrCTxlMsuOmP7tlxhdRgGIrzP8bhhbceTRlfIM4BDuQNkWs1LfYoeb5sUaNXKTgD802xQiHTMGMhncZfHM0l2pw07b0dTR4pU07EES+APYt5FoELvhn4BxB0Ci5qGNiX3Dv71i1ceSizpqgDR/hhbEVeuum2drdEkCF4TU6ZHO+/B4VawYEMAtJLRRFpmUWEF5EngsuaWd3hKfRFjMEEOA7Q2JrWxP2OGRLwzGN4eMalZTMYbkSp+QlJ1Q9xpzSR+y2tqocHXvVMBMaM5yjLem1tf2McsKiYIQfeKrYHQhJJK35pLSkd5qhl6SWOQrNET2iEgQm9hOL1zW7JBFk/05DuRQB8SYHbji5Cnqlj0urLTQvJWWsmkfBusleQVxX17AbFo9RbZnZumDCFJkOd++NpQS7cnpWUrJtxgX+iCX/dbyqU39ADJEtK77632tEptJSnqazMAnnEYhG0OnPYG40691iT7ojmWoxVvhyFGN+xbrCqTvNNMrlaf/JQN/wfP1ClmQqjdFUpKUMOfcoMJnU3s8NMcQlS3ABqKxscD/pPpOVs6u1ZlQnPdp3XYt3gUwhyFpUXRArUzkyk2G5dTJCC1k9jwfkP2Nz/AKk76DVGne10v7o5SqT/NniuaxexOgmUmjwZo1LQPELoJQ5944slUQavntnU4KtKvpkP235l9NjoxnMTB4gcW9quoqsgbmZ8s68rAJjUPiHDfIiKEuVZpzsv1nZQUkqqb1nov4zWWrBqpG607z2tV6MmpA+tlnwoytHjRYOEKzUNxMkbZHLIQMTGWdXnsRYrwkGepi2GiPmvNV0YeWnb08eW2EZ6KomGYhG/HqRBUwAk5O86cpellucE9Wk3aiYNAKPIImPvg6H8I9zxGzBJrFfKV1pZPMEPHHiQTOZ+388ZgGkNpbWxqJC1WFCwKc+qaqNngu4WJ1FhcbmhYYAhS00GWKqNlNuOYm/80p+83p3J50yzddBBOAP5nswXU/E8xFnHE445aj+fvHbEgF249vOTvFgEvctJoBsBdIK84IL1qzk94SYapqupzUHYZFvECZDuz0s5AgF4dM5UWzL37tZnjBK0uBMNLd0EmkKw++GYTy5PwFrrilEwiUSqbineurvJjbyEurBi0aytZEiOSsJwDw6jnvxOVfd2OFjmc70np4meKz1xrewyscto0Vkjw7k/9dOQTlRAwsTqAWkgyAAQK1jxP4VJYIxzUqA9krmWifViPNbRHzHIdpSC8ngkZXJE89/q6my4ADxfTRG3zy8ED2ZTrSJYsw8KA4SGqV3+MeRjAg9jaXFXxdNlpBE+TbpnS7kqMK035QyxCHej187uHbqfmS8cWdXTK6T5sMUdgdWw+TpEp3JaVS7fRRjED8O4VkVRUFs9EsWmL6CbuldGJYv1Gu9jISW3zeMkmQxPdOeC3HHqO14/gcHjIZZRv7RqWJDr4LDAjw6DDIfSM4KERrsvNuvkIsZLGmx5L6ZhL3BG/X0KJ2SF2iKkKn+/CHMha/oJwP0ob78GetwByVW2lRImccKjGM0daOS9n9OIGTtju6Mvj2/VYXemHGxXmcT/Q064/KlTi010n4+bAUsqDHJxgKCfADLCiiVFiI7W7mZldJ5URHJNORb6TM2p9YN8E45+7KWw+OUg2MVFpG53I0dWea7tS97GyGIr+scwkLSl5WT9Pct7zVeDTGT8yxlxBJkR6RBqcIxstbqAPofQJfXHTn/9NdXCLHrVIABUS9QcmRszlCyE/vt50bU5tnaM7ynda/t4fK/JYzlfA2ZkJSjdQMM3Ke0hhPAWTQEV4pH6lH5lzTFkdyHdnCt/Ck5654fbSR6KGqb8zXqZ+kW7w2pbULXf7bAcrOy5my7uxmo6GdusS1TG70Zh9dNH1fdsKH+xFHVYF5aofOQ==", + "verificationMethod": "did:key:zUC7DVPRfshooBqmnT2LrMxabCUkRhyyUCu8xKvYRot5aeTLTpPxzZoMyFkMLgKHMPUzdEnJM1EqbxfQd466ed3QuEtUJr8iqKRVfJ4txBa3PRoASaup6fjVAkU9VdbDbs5et64#zUC7DVPRfshooBqmnT2LrMxabCUkRhyyUCu8xKvYRot5aeTLTpPxzZoMyFkMLgKHMPUzdEnJM1EqbxfQd466ed3QuEtUJr8iqKRVfJ4txBa3PRoASaup6fjVAkU9VdbDbs5et64", + "proofPurpose": "assertionMethod", + "created": "2021-09-27T10:40:03.200843+00:00", + }, + } + constraint = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient.birthDate"], + "id": "birthDate", + "purpose": "Датум рођења", + } + ], + } + constraint = Constraints.deserialize(constraint) + assert await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) From ae63a2d919962412bdbc993dfe90152a9963a24f Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Sat, 2 Oct 2021 09:22:10 -0700 Subject: [PATCH 02/29] update mandatory field_paths and contraint path in case of selective disclosure Signed-off-by: Shaanjot Gill --- .../present_proof/dif/pres_exch_handler.py | 33 ++++-- .../dif/tests/test_pres_exch_handler.py | 107 +++++++++++++++++- 2 files changed, 125 insertions(+), 15 deletions(-) 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 88115a03a4..68a199867c 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -1250,7 +1250,7 @@ def check_if_cred_id_derived(self, id: str) -> bool: async def merge( self, dict_descriptor_creds: dict, - ) -> (Sequence[VCRecord], Sequence[InputDescriptorMapping]): + ) -> Tuple[Sequence[VCRecord], Sequence[InputDescriptorMapping]]: """ Return applicable credentials and descriptor_map for attachment. @@ -1324,6 +1324,15 @@ async def verify_received_pres( f"apply to the enclosed credential in {desc_map_item_path}" ) + def get_field_updated_path(self, field: DIFField) -> DIFField: + """Update DIFField path to remove any [*] in case of limit_disclosure.""" + given_paths = field.paths + updated_paths = [] + for path in given_paths: + updated_paths.append(re.sub(r"\[\d+\]", "", path)) + field.paths = updated_paths + return field + async def apply_constraint_received_cred( self, constraint: Constraints, cred_dict: dict ) -> bool: @@ -1331,12 +1340,15 @@ async def apply_constraint_received_cred( fields = constraint._fields field_paths = [] credential = self.create_vcrecord(cred_dict) + is_limit_disclosure = constraint.limit_disclosure == "required" for field in fields: + if is_limit_disclosure: + field = self.get_field_updated_path(field) field_paths = field_paths + field.paths if not await self.filter_by_field(field, credential): return False # Selective Disclosure check - if constraint.limit_disclosure == "required": + if is_limit_disclosure: field_paths = set([path.replace("$.", "") for path in field_paths]) mandatory_paths = { "@context", @@ -1354,11 +1366,8 @@ async def apply_constraint_received_cred( split_field_path = field_path.split(".") key = ".".join(split_field_path[:-1]) value = split_field_path[-1] - additional_attrs = self.get_dict_keys_from_path( - cred_dict, key.split(".") - ) nested_field_paths = self.build_nested_paths_dict( - key, value, nested_field_paths, additional_attrs + key, value, nested_field_paths, cred_dict ) to_remove_from_field_paths.add(field_path) for to_remove_path in to_remove_from_field_paths: @@ -1384,7 +1393,7 @@ def get_dict_keys_from_path(self, derived_cred_dict: dict, path: str) -> list: return self.get_dict_keys_from_path(derived_cred_dict[path[0]], path[1:]) else: additional_attrs = [] - mandatory_paths = ["@id", "id", "type"] + mandatory_paths = ["@id", "@type"] keys = derived_cred_dict.keys() for key in keys: if key in mandatory_paths: @@ -1403,21 +1412,25 @@ def build_nested_paths_dict( key: str, value: str, nested_field_paths: dict, - additional_attrs: list = None, + cred_dict: dict, ) -> dict: """Build and return nested_field_paths dict.""" if key in nested_field_paths.keys(): nested_field_paths[key].add(value) else: nested_field_paths[key] = {value} - if additional_attrs and len(additional_attrs) > 0: + additional_attrs = self.get_dict_keys_from_path(cred_dict, key.split(".")) + if len(additional_attrs) > 0: for attr in additional_attrs: nested_field_paths[key].add(attr) split_key = key.split(".") if len(split_key) > 1: nested_field_paths.update( self.build_nested_paths_dict( - ".".join(split_key[:-1]), split_key[-1], nested_field_paths + ".".join(split_key[:-1]), + split_key[-1], + nested_field_paths, + cred_dict, ) ) return nested_field_paths 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 bd86a1c804..9b9d534385 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 @@ -3152,7 +3152,7 @@ async def test_is_holder_missing_subject(self, profile, setup_tuple): @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures - async def test_apply_constraint_received_cred(self, profile): + async def test_apply_constraint_received_cred_invalid(self, profile): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type ) @@ -3187,16 +3187,16 @@ async def test_apply_constraint_received_cred(self, profile): "Patient": { "@id": "urn:bnid:_:c14n2", "type": "fhir:resource-types#Patient", - "birthDate": "1958-12-03T00:00:00", + "birthDate": "1978-12-06T00:00:00", }, }, "issuanceDate": "2021-09-27T12:40:03+02:00", - "issuer": "did:key:zUC7DVPRfshooBqmnT2LrMxabCUkRhyyUCu8xKvYRot5aeTLTpPxzZoMyFkMLgKHMPUzdEnJM1EqbxfQd466ed3QuEtUJr8iqKRVfJ4txBa3PRoASaup6fjVAkU9VdbDbs5et64", + "issuer": "did:key:test", "proof": { "type": "BbsBlsSignatureProof2020", "nonce": "XVdQwHnUYJkkMX4LDWFPVuB7NQJ5IVn6ohW/psGv3fFSJ9kbr59BcWpix7Q3LBfzJ80=", "proofValue": "ADgAAAAQAnwED6993W+P6/Iptfdk1uEaOcKDEYYJVjR154bB9r+skTZFR/P82oKJpMzZtMD+jRLV1Y4STy/JCg2nuKQVkyNbBTOxnJ5gNrYH7ZVC0IkQbq16Ld9bPkS7JeU2pfQ1fDZAL6CrkX+k7E24FFFe+ssLrkKLTDWe/J94+MJW/3qGAIuOKQ3t739ILPkElMHdNaxLNAAAAHSKc0EckEFyjM7bBqlxaLJhxkU2kcyR5ZwBee9UGzD+2s3J90rDvg4KUw7d4eli2SAAAAACGXrTBdx3eL+ki5OUBqpQ3k9oYMIwifYJdhtE4BfdMhgVwTWuNcXgXXvV46vOB1uJLi2/zaWzhAWDY9EGRM0nhot3dail73ts2KYPa+/pUMnWlmF5tVYzJNIQcPiIFzO27wA1mnFy7VXmbgHpGUmwqgAAAC5uHM39yBidgmkXY+zhOQJSC8CAJ1R2VLlOg+N8eOecIGSqaQGE7zTKrCTxlMsuOmP7tlxhdRgGIrzP8bhhbceTRlfIM4BDuQNkWs1LfYoeb5sUaNXKTgD802xQiHTMGMhncZfHM0l2pw07b0dTR4pU07EES+APYt5FoELvhn4BxB0Ci5qGNiX3Dv71i1ceSizpqgDR/hhbEVeuum2drdEkCF4TU6ZHO+/B4VawYEMAtJLRRFpmUWEF5EngsuaWd3hKfRFjMEEOA7Q2JrWxP2OGRLwzGN4eMalZTMYbkSp+QlJ1Q9xpzSR+y2tqocHXvVMBMaM5yjLem1tf2McsKiYIQfeKrYHQhJJK35pLSkd5qhl6SWOQrNET2iEgQm9hOL1zW7JBFk/05DuRQB8SYHbji5Cnqlj0urLTQvJWWsmkfBusleQVxX17AbFo9RbZnZumDCFJkOd++NpQS7cnpWUrJtxgX+iCX/dbyqU39ADJEtK77632tEptJSnqazMAnnEYhG0OnPYG40691iT7ojmWoxVvhyFGN+xbrCqTvNNMrlaf/JQN/wfP1ClmQqjdFUpKUMOfcoMJnU3s8NMcQlS3ABqKxscD/pPpOVs6u1ZlQnPdp3XYt3gUwhyFpUXRArUzkyk2G5dTJCC1k9jwfkP2Nz/AKk76DVGne10v7o5SqT/NniuaxexOgmUmjwZo1LQPELoJQ5944slUQavntnU4KtKvpkP235l9NjoxnMTB4gcW9quoqsgbmZ8s68rAJjUPiHDfIiKEuVZpzsv1nZQUkqqb1nov4zWWrBqpG607z2tV6MmpA+tlnwoytHjRYOEKzUNxMkbZHLIQMTGWdXnsRYrwkGepi2GiPmvNV0YeWnb08eW2EZ6KomGYhG/HqRBUwAk5O86cpellucE9Wk3aiYNAKPIImPvg6H8I9zxGzBJrFfKV1pZPMEPHHiQTOZ+388ZgGkNpbWxqJC1WFCwKc+qaqNngu4WJ1FhcbmhYYAhS00GWKqNlNuOYm/80p+83p3J50yzddBBOAP5nswXU/E8xFnHE445aj+fvHbEgF249vOTvFgEvctJoBsBdIK84IL1qzk94SYapqupzUHYZFvECZDuz0s5AgF4dM5UWzL37tZnjBK0uBMNLd0EmkKw++GYTy5PwFrrilEwiUSqbineurvJjbyEurBi0aytZEiOSsJwDw6jnvxOVfd2OFjmc70np4meKz1xrewyscto0Vkjw7k/9dOQTlRAwsTqAWkgyAAQK1jxP4VJYIxzUqA9krmWifViPNbRHzHIdpSC8ngkZXJE89/q6my4ADxfTRG3zy8ED2ZTrSJYsw8KA4SGqV3+MeRjAg9jaXFXxdNlpBE+TbpnS7kqMK035QyxCHej187uHbqfmS8cWdXTK6T5sMUdgdWw+TpEp3JaVS7fRRjED8O4VkVRUFs9EsWmL6CbuldGJYv1Gu9jISW3zeMkmQxPdOeC3HHqO14/gcHjIZZRv7RqWJDr4LDAjw6DDIfSM4KERrsvNuvkIsZLGmx5L6ZhL3BG/X0KJ2SF2iKkKn+/CHMha/oJwP0ob78GetwByVW2lRImccKjGM0daOS9n9OIGTtju6Mvj2/VYXemHGxXmcT/Q064/KlTi010n4+bAUsqDHJxgKCfADLCiiVFiI7W7mZldJ5URHJNORb6TM2p9YN8E45+7KWw+OUg2MVFpG53I0dWea7tS97GyGIr+scwkLSl5WT9Pct7zVeDTGT8yxlxBJkR6RBqcIxstbqAPofQJfXHTn/9NdXCLHrVIABUS9QcmRszlCyE/vt50bU5tnaM7ynda/t4fK/JYzlfA2ZkJSjdQMM3Ke0hhPAWTQEV4pH6lH5lzTFkdyHdnCt/Ck5654fbSR6KGqb8zXqZ+kW7w2pbULXf7bAcrOy5my7uxmo6GdusS1TG70Zh9dNH1fdsKH+xFHVYF5aofOQ==", - "verificationMethod": "did:key:zUC7DVPRfshooBqmnT2LrMxabCUkRhyyUCu8xKvYRot5aeTLTpPxzZoMyFkMLgKHMPUzdEnJM1EqbxfQd466ed3QuEtUJr8iqKRVfJ4txBa3PRoASaup6fjVAkU9VdbDbs5et64#zUC7DVPRfshooBqmnT2LrMxabCUkRhyyUCu8xKvYRot5aeTLTpPxzZoMyFkMLgKHMPUzdEnJM1EqbxfQd466ed3QuEtUJr8iqKRVfJ4txBa3PRoASaup6fjVAkU9VdbDbs5et64", + "verificationMethod": "did:key:test", "proofPurpose": "assertionMethod", "created": "2021-09-27T10:40:03.200843+00:00", }, @@ -3207,7 +3207,79 @@ async def test_apply_constraint_received_cred(self, profile): { "path": ["$.credentialSubject.Patient.birthDate"], "id": "birthDate", - "purpose": "Датум рођења", + "purpose": "test", + } + ], + } + constraint = Constraints.deserialize(constraint) + assert not await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_apply_constraint_received_cred_valid(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_dict = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/bbs/v1", + { + "MedicalPass": { + "@id": "https://www.vdel.com/MedicalPass", + "@context": { + "description": "http://schema.org/description", + "identifier": "http://schema.org/identifier", + "name": "http://schema.org/name", + "image": "http://schema.org/image", + }, + } + }, + { + "Patient": { + "@id": "http://hl7.org/fhir/Patient", + "@context": [ + "https://fhircat.org/fhir-r5/rdf-r5/contexts/patient.context.jsonld" + ], + } + }, + ], + "id": "urn:bnid:_:c14n4", + "type": ["MedicalPass", "VerifiableCredential"], + "credentialSubject": { + "id": "urn:bnid:_:c14n6", + "Patient": { + "@id": "urn:bnid:_:c14n7", + "@type": "fhir:resource-types#Patient", + "address": { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + "country": "RS", + "line": "test 81", + "postalCode": "1564", + "type": "both", + }, + }, + }, + "issuanceDate": "2021-10-01T20:16:40+02:00", + "issuer": "did:key:test", + "proof": { + "type": "BbsBlsSignatureProof2020", + "nonce": "M9yQx0eKIAI3Zs0sLF1kQsO7/hV1ZKEnqX9f0V/SzwRMKEixa0tJgqbGwviMA04XoL0=", + "proofValue": "ACYgFHwPj5TxR9H+k+V+rBsfZ3SgOEvoKrYCcvAl4HhaKNR039r5UWE89tnHaVOx22k604EWibf0s7BTezijjYv1VWSVkZar4wtOslplXv6g7dVc8/0IWXQWOfn2hTE2N65Wv8xz2qw5dWwEzSXTx44o15wE2ubimgGFMM7Mv++SAoHC1dQGotGqKqOB2PS8yI+ToiWmswAAAHSD5NRIZHKeiWP8hK/e9xUYy5gSPBivDVAORybl62B/F3eaUC/pRdfsORAWRHLjmfcAAAACcOe6yrLqI3OmxkKUfsCGgIl83LLcQ9pLjaigdc/5XRs6KYo533Q/7cGryn2IvLFAJiHgZJ8Ovwi9xkDy1USKjZfjgRMil4PEiwZ2Gqu4g+HlJ11JemUX2HDAjJYgJHSFguZp/l/5y//0pQegHOi9hwAAABcp9nblpM/ALrFpdenGn23x5kdYC4gMyTV6a6RPuMwryVZcmTP50XDVHiY2t4JLvULdJcGDcOCpetMPhqyAf3VeNtorYjr1+YWSgjApfqZ594rMyohWGwkNu0zqv19qDkQ+aBibGhhsCBHe+jFy/BXQv2TlIMgX7YdUgVtUuO4YJT4cz4xrDlK58sJPpmJqraasoA0E+ciPOtGX5J7e4n+dGlPwkQjcD79cjBGs7hXmljeqbe2a82YQw/Q+L/yVKqxl8+ucLoqQ2QzREKslQ7ljchX8RsHQURflZTgPxGjNyCqHtEIcT6d7COcpmqGYSo5ge0pIXab97H97NBnk9mmdcCOCETWOJ8shuS7n4R4GdnRDjB5ArbBnpIMYUGEsdD0ZR87nVBbAfWFhQWJgsJvpPOGq2p6VPImfwhIoh7LIYkpwVogRLrSQGl5IZcHexlHwjZoogafCD5OSyEAO3au3UUoVde4S98v2233QuOwXvz3ptYOO+aJIbqmgdmGs41YfbyT830/H+248+Zbkob7T1FBWbYtEW+k8omat87tc3RfU9LYgNrXWUpJ/TZ+4Cqg7VljkPhCIEZYNUoKQxG1pP11HsmLvzhtnoNVLwjvJA7IrcinAr2pnWSBzjm/wBx8mANrCAHW4f4yyvSXCWZJOfnf/N8dt01Di0QaNbYs8Hlo6yjjjqkrvgLpZtAuuca8nQPPNZWrj3Oids/Z0nZsgKGwZxHo5negKE1JKEEz7zJQUd14JhRYiwfzWYprHcJ9szp5Tgmskksv3NIyKQ7XfLwnOY29zLOpTm51c99Ru6CVvAvIGckB+oE8cwPRjfE9fajJtQEODZ1ljbzYNACzLZ52iSsL+rSKq9LL79TgmN2lE0SkmgrwkOBAjmSwzrBc9DdQrkpWlSZzOWyL/QuNfHfEiNn43nwhaJpbvQ6zr/XHbspH7oqe0eexfvzowzkKc9noWqQnU0IaMrtRgyOma", + "verificationMethod": "did:key:test", + "proofPurpose": "assertionMethod", + "created": "2021-10-01T18:16:41.072975+00:00", + }, + } + constraint = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient.address"], + "purpose": "Test", } ], } @@ -3215,3 +3287,28 @@ async def test_apply_constraint_received_cred(self, profile): assert await dif_pres_exch_handler.apply_constraint_received_cred( constraint=constraint, cred_dict=cred_dict ) + + def test_get_field_updated_path(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + original_field = DIFField.deserialize( + {"path": ["$.credentialSubject.Patient.address[0].city"], "purpose": "Test"} + ) + updated_field = dif_pres_exch_handler.get_field_updated_path(original_field) + assert "$.credentialSubject.Patient.address.city" in updated_field.paths + assert "$.credentialSubject.Patient.address[0].city" not in updated_field.paths + original_field = DIFField.deserialize( + { + "path": [ + "$.credentialSubject.Patient.birthDate", + "$.credentialSubject.Patient.address[0].city", + ], + "purpose": "Test", + } + ) + updated_field = dif_pres_exch_handler.get_field_updated_path(original_field) + + assert "$.credentialSubject.Patient.address[0].city" not in updated_field.paths + assert "$.credentialSubject.Patient.address.city" in updated_field.paths + assert "$.credentialSubject.Patient.birthDate" in updated_field.paths From 1280b0e0348a4c426a7959baa94e0c1f1f113ba6 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Mon, 4 Oct 2021 23:44:41 -0700 Subject: [PATCH 03/29] updates Signed-off-by: Shaanjot Gill --- .../present_proof/dif/pres_exch_handler.py | 92 ++++++--- .../present_proof/dif/tests/test_data.py | 49 +++++ .../dif/tests/test_pres_exch_handler.py | 179 ++++++++++++------ 3 files changed, 235 insertions(+), 85 deletions(-) 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 68a199867c..de0db781b0 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -11,13 +11,14 @@ import pytz import re +from copy import deepcopy from datetime import datetime from dateutil.parser import parse as dateutil_parser from dateutil.parser import ParserError from jsonpath_ng import parse from pyld import jsonld from pyld.jsonld import JsonLdProcessor -from typing import Sequence, Optional, Tuple +from typing import Sequence, Optional, Tuple, Union, Dict, List from unflatten import unflatten from uuid import uuid4 @@ -582,8 +583,11 @@ async def filter_by_field(self, field: DIFField, credential: VCRecord) -> bool: "JSON Path expression matching on proof object " "is not currently supported" ) - jsonpath = parse(path) - match = jsonpath.find(credential_dict) + try: + jsonpath = parse(path) + match = jsonpath.find(credential_dict) + except KeyError: + continue if len(match) == 0: continue for match_item in match: @@ -1324,14 +1328,15 @@ async def verify_received_pres( f"apply to the enclosed credential in {desc_map_item_path}" ) - def get_field_updated_path(self, field: DIFField) -> DIFField: + def get_field_updated_path(self, field: DIFField) -> Tuple[DIFField, DIFField]: """Update DIFField path to remove any [*] in case of limit_disclosure.""" + new_field = deepcopy(field) given_paths = field.paths - updated_paths = [] + new_paths = [] for path in given_paths: - updated_paths.append(re.sub(r"\[\d+\]", "", path)) - field.paths = updated_paths - return field + new_paths.append(re.sub(r"\[(\W+)\]|\[(\d+)\]", "", path)) + new_field.paths = new_paths + return (field, new_field) async def apply_constraint_received_cred( self, constraint: Constraints, cred_dict: dict @@ -1343,10 +1348,17 @@ async def apply_constraint_received_cred( is_limit_disclosure = constraint.limit_disclosure == "required" for field in fields: if is_limit_disclosure: - field = self.get_field_updated_path(field) + field, new_field = self.get_field_updated_path(field) + original_field_filter = await self.filter_by_field(field, credential) + new_field_filter = await self.filter_by_field(new_field, credential) + if not original_field_filter and not new_field_filter: + return False + elif not original_field_filter and new_field_filter: + field = new_field + else: + if not await self.filter_by_field(field, credential): + return False field_paths = field_paths + field.paths - if not await self.filter_by_field(field, credential): - return False # Selective Disclosure check if is_limit_disclosure: field_paths = set([path.replace("$.", "") for path in field_paths]) @@ -1365,6 +1377,7 @@ async def apply_constraint_received_cred( if field_path.count(".") >= 1: split_field_path = field_path.split(".") key = ".".join(split_field_path[:-1]) + key = re.sub(r"\[(\W+)\]|\[(\d+)\]", "", key) value = split_field_path[-1] nested_field_paths = self.build_nested_paths_dict( key, value, nested_field_paths, cred_dict @@ -1381,31 +1394,54 @@ async def apply_constraint_received_cred( for nested_attr_key in nested_field_paths: nested_attr_values = nested_field_paths[nested_attr_key] split_nested_attr_key = nested_attr_key.split(".") - extracted_dict = self.nested_get(cred_dict, split_nested_attr_key) - for attrs in extracted_dict.keys(): - if attrs not in nested_attr_values: + extracted = self.nested_get(cred_dict, split_nested_attr_key) + if isinstance(extracted, dict): + if not self.check_attr_in_extracted_dict( + extracted, nested_attr_values + ): return False + elif isinstance(extracted, list): + for extracted_dict in extracted: + if not self.check_attr_in_extracted_dict( + extracted_dict, nested_attr_values + ): + return False + return True + + def check_attr_in_extracted_dict( + self, extracted_dict: dict, nested_attr_values: dict + ) -> bool: + """Check if keys of extracted_dict exists in nested_attr_values.""" + for attrs in extracted_dict.keys(): + if attrs not in nested_attr_values: + return False return True def get_dict_keys_from_path(self, derived_cred_dict: dict, path: str) -> list: """Return list of keys as additional_attrs to build nested_field_paths.""" - if path: - return self.get_dict_keys_from_path(derived_cred_dict[path[0]], path[1:]) - else: - additional_attrs = [] - mandatory_paths = ["@id", "@type"] - keys = derived_cred_dict.keys() - for key in keys: + additional_attrs = [] + mandatory_paths = ["@id", "@type"] + jsonpath = parse(path) + match = jsonpath.find(derived_cred_dict) + match_item = match[0].value + if isinstance(match_item, dict): + for key in match_item.keys(): + if key in mandatory_paths: + additional_attrs.append(key) + elif isinstance(match_item, list): + for key in match_item[0].keys(): if key in mandatory_paths: additional_attrs.append(key) - return additional_attrs + return additional_attrs - def nested_get(self, input_dict: dict, nested_key: Sequence[str]) -> dict: - """Return internal dict from nested input_dict given list of nested_key.""" - internal_dict_value = input_dict + def nested_get( + self, input_dict: dict, nested_key: Sequence[str] + ) -> Union[Dict, List]: + """Return dict or list from nested dict given list of nested_key.""" + internal_value = input_dict for k in nested_key: - internal_dict_value = internal_dict_value.get(k, None) - return internal_dict_value + internal_value = internal_value.get(k, None) + return internal_value def build_nested_paths_dict( self, @@ -1419,7 +1455,7 @@ def build_nested_paths_dict( nested_field_paths[key].add(value) else: nested_field_paths[key] = {value} - additional_attrs = self.get_dict_keys_from_path(cred_dict, key.split(".")) + additional_attrs = self.get_dict_keys_from_path(cred_dict, key) if len(additional_attrs) > 0: for attr in additional_attrs: nested_field_paths[key].add(attr) 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 f45a09743d..30b9074366 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py @@ -1500,6 +1500,55 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): } """ +TEST_CRED_DICT = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/bbs/v1", + { + "MedicalPass": { + "@id": "https://www.vdel.com/MedicalPass", + "@context": { + "description": "http://schema.org/description", + "identifier": "http://schema.org/identifier", + "name": "http://schema.org/name", + "image": "http://schema.org/image", + }, + } + }, + { + "Patient": { + "@id": "http://hl7.org/fhir/Patient", + "@context": [ + "https://fhircat.org/fhir-r5/rdf-r5/contexts/patient.context.jsonld" + ], + } + }, + ], + "id": "urn:bnid:_:c14n4", + "type": ["MedicalPass", "VerifiableCredential"], + "credentialSubject": { + "id": "urn:bnid:_:c14n6", + "Patient": { + "@id": "urn:bnid:_:c14n7", + "@type": "fhir:resource-types#Patient", + "address": { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + }, + }, + "issuanceDate": "2021-10-01T20:16:40+02:00", + "issuer": "did:key:test", + "proof": { + "type": "BbsBlsSignatureProof2020", + "nonce": "M9yQx0eKIAI3Zs0sLF1kQsO7/hV1ZKEnqX9f0V/SzwRMKEixa0tJgqbGwviMA04XoL0=", + "proofValue": "ACYgFHwPj5TxR9H+k+V+rBsfZ3SgOEvoKrYCcvAl4HhaKNR039r5UWE89tnHaVOx22k604EWibf0s7BTezijjYv1VWSVkZar4wtOslplXv6g7dVc8/0IWXQWOfn2hTE2N65Wv8xz2qw5dWwEzSXTx44o15wE2ubimgGFMM7Mv++SAoHC1dQGotGqKqOB2PS8yI+ToiWmswAAAHSD5NRIZHKeiWP8hK/e9xUYy5gSPBivDVAORybl62B/F3eaUC/pRdfsORAWRHLjmfcAAAACcOe6yrLqI3OmxkKUfsCGgIl83LLcQ9pLjaigdc/5XRs6KYo533Q/7cGryn2IvLFAJiHgZJ8Ovwi9xkDy1USKjZfjgRMil4PEiwZ2Gqu4g+HlJ11JemUX2HDAjJYgJHSFguZp/l/5y//0pQegHOi9hwAAABcp9nblpM/ALrFpdenGn23x5kdYC4gMyTV6a6RPuMwryVZcmTP50XDVHiY2t4JLvULdJcGDcOCpetMPhqyAf3VeNtorYjr1+YWSgjApfqZ594rMyohWGwkNu0zqv19qDkQ+aBibGhhsCBHe+jFy/BXQv2TlIMgX7YdUgVtUuO4YJT4cz4xrDlK58sJPpmJqraasoA0E+ciPOtGX5J7e4n+dGlPwkQjcD79cjBGs7hXmljeqbe2a82YQw/Q+L/yVKqxl8+ucLoqQ2QzREKslQ7ljchX8RsHQURflZTgPxGjNyCqHtEIcT6d7COcpmqGYSo5ge0pIXab97H97NBnk9mmdcCOCETWOJ8shuS7n4R4GdnRDjB5ArbBnpIMYUGEsdD0ZR87nVBbAfWFhQWJgsJvpPOGq2p6VPImfwhIoh7LIYkpwVogRLrSQGl5IZcHexlHwjZoogafCD5OSyEAO3au3UUoVde4S98v2233QuOwXvz3ptYOO+aJIbqmgdmGs41YfbyT830/H+248+Zbkob7T1FBWbYtEW+k8omat87tc3RfU9LYgNrXWUpJ/TZ+4Cqg7VljkPhCIEZYNUoKQxG1pP11HsmLvzhtnoNVLwjvJA7IrcinAr2pnWSBzjm/wBx8mANrCAHW4f4yyvSXCWZJOfnf/N8dt01Di0QaNbYs8Hlo6yjjjqkrvgLpZtAuuca8nQPPNZWrj3Oids/Z0nZsgKGwZxHo5negKE1JKEEz7zJQUd14JhRYiwfzWYprHcJ9szp5Tgmskksv3NIyKQ7XfLwnOY29zLOpTm51c99Ru6CVvAvIGckB+oE8cwPRjfE9fajJtQEODZ1ljbzYNACzLZ52iSsL+rSKq9LL79TgmN2lE0SkmgrwkOBAjmSwzrBc9DdQrkpWlSZzOWyL/QuNfHfEiNn43nwhaJpbvQ6zr/XHbspH7oqe0eexfvzowzkKc9noWqQnU0IaMrtRgyOma", + "verificationMethod": "did:key:test", + "proofPurpose": "assertionMethod", + "created": "2021-10-01T18:16:41.072975+00:00", + }, +} + def get_test_data(): vc_record_list = [] 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 9b9d534385..db6e286010 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 @@ -51,6 +51,7 @@ is_holder_pd, is_holder_pd_multiple_fields_excluded, is_holder_pd_multiple_fields_included, + TEST_CRED_DICT, ) @@ -3152,7 +3153,7 @@ async def test_is_holder_missing_subject(self, profile, setup_tuple): @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures - async def test_apply_constraint_received_cred_invalid(self, profile): + async def test_apply_constraint_received_cred_invalid_a(self, profile): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type ) @@ -3218,67 +3219,98 @@ async def test_apply_constraint_received_cred_invalid(self, profile): @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures - async def test_apply_constraint_received_cred_valid(self, profile): + async def test_apply_constraint_received_cred_invalid_b(self, profile): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type ) - cred_dict = { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/bbs/v1", + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"]["Patient"]["address"] = [ + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + { + "country": "Рума", + }, + ] + constraint = { + "limit_disclosure": "required", + "fields": [ { - "MedicalPass": { - "@id": "https://www.vdel.com/MedicalPass", - "@context": { - "description": "http://schema.org/description", - "identifier": "http://schema.org/identifier", - "name": "http://schema.org/name", - "image": "http://schema.org/image", - }, - } - }, + "path": ["$.credentialSubject.Patient.address[0].city"], + "purpose": "Test", + } + ], + } + constraint = Constraints.deserialize(constraint) + assert not await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_apply_constraint_received_cred_valid_a(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_dict = deepcopy(TEST_CRED_DICT) + constraint = { + "limit_disclosure": "required", + "fields": [ { - "Patient": { - "@id": "http://hl7.org/fhir/Patient", - "@context": [ - "https://fhircat.org/fhir-r5/rdf-r5/contexts/patient.context.jsonld" - ], - } - }, + "path": ["$.credentialSubject.Patient.address"], + "purpose": "Test", + } ], - "id": "urn:bnid:_:c14n4", - "type": ["MedicalPass", "VerifiableCredential"], - "credentialSubject": { - "id": "urn:bnid:_:c14n6", - "Patient": { - "@id": "urn:bnid:_:c14n7", - "@type": "fhir:resource-types#Patient", - "address": { - "@id": "urn:bnid:_:c14n1", - "city": "Рума", - "country": "RS", - "line": "test 81", - "postalCode": "1564", - "type": "both", - }, - }, + } + constraint = Constraints.deserialize(constraint) + assert await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_apply_constraint_received_cred_valid_b(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_dict = deepcopy(TEST_CRED_DICT) + constraint = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient.address[0].city"], + "purpose": "Test", + } + ], + } + constraint = Constraints.deserialize(constraint) + assert await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_apply_constraint_received_cred_valid_c(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"]["Patient"]["address"] = [ + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", }, - "issuanceDate": "2021-10-01T20:16:40+02:00", - "issuer": "did:key:test", - "proof": { - "type": "BbsBlsSignatureProof2020", - "nonce": "M9yQx0eKIAI3Zs0sLF1kQsO7/hV1ZKEnqX9f0V/SzwRMKEixa0tJgqbGwviMA04XoL0=", - "proofValue": "ACYgFHwPj5TxR9H+k+V+rBsfZ3SgOEvoKrYCcvAl4HhaKNR039r5UWE89tnHaVOx22k604EWibf0s7BTezijjYv1VWSVkZar4wtOslplXv6g7dVc8/0IWXQWOfn2hTE2N65Wv8xz2qw5dWwEzSXTx44o15wE2ubimgGFMM7Mv++SAoHC1dQGotGqKqOB2PS8yI+ToiWmswAAAHSD5NRIZHKeiWP8hK/e9xUYy5gSPBivDVAORybl62B/F3eaUC/pRdfsORAWRHLjmfcAAAACcOe6yrLqI3OmxkKUfsCGgIl83LLcQ9pLjaigdc/5XRs6KYo533Q/7cGryn2IvLFAJiHgZJ8Ovwi9xkDy1USKjZfjgRMil4PEiwZ2Gqu4g+HlJ11JemUX2HDAjJYgJHSFguZp/l/5y//0pQegHOi9hwAAABcp9nblpM/ALrFpdenGn23x5kdYC4gMyTV6a6RPuMwryVZcmTP50XDVHiY2t4JLvULdJcGDcOCpetMPhqyAf3VeNtorYjr1+YWSgjApfqZ594rMyohWGwkNu0zqv19qDkQ+aBibGhhsCBHe+jFy/BXQv2TlIMgX7YdUgVtUuO4YJT4cz4xrDlK58sJPpmJqraasoA0E+ciPOtGX5J7e4n+dGlPwkQjcD79cjBGs7hXmljeqbe2a82YQw/Q+L/yVKqxl8+ucLoqQ2QzREKslQ7ljchX8RsHQURflZTgPxGjNyCqHtEIcT6d7COcpmqGYSo5ge0pIXab97H97NBnk9mmdcCOCETWOJ8shuS7n4R4GdnRDjB5ArbBnpIMYUGEsdD0ZR87nVBbAfWFhQWJgsJvpPOGq2p6VPImfwhIoh7LIYkpwVogRLrSQGl5IZcHexlHwjZoogafCD5OSyEAO3au3UUoVde4S98v2233QuOwXvz3ptYOO+aJIbqmgdmGs41YfbyT830/H+248+Zbkob7T1FBWbYtEW+k8omat87tc3RfU9LYgNrXWUpJ/TZ+4Cqg7VljkPhCIEZYNUoKQxG1pP11HsmLvzhtnoNVLwjvJA7IrcinAr2pnWSBzjm/wBx8mANrCAHW4f4yyvSXCWZJOfnf/N8dt01Di0QaNbYs8Hlo6yjjjqkrvgLpZtAuuca8nQPPNZWrj3Oids/Z0nZsgKGwZxHo5negKE1JKEEz7zJQUd14JhRYiwfzWYprHcJ9szp5Tgmskksv3NIyKQ7XfLwnOY29zLOpTm51c99Ru6CVvAvIGckB+oE8cwPRjfE9fajJtQEODZ1ljbzYNACzLZ52iSsL+rSKq9LL79TgmN2lE0SkmgrwkOBAjmSwzrBc9DdQrkpWlSZzOWyL/QuNfHfEiNn43nwhaJpbvQ6zr/XHbspH7oqe0eexfvzowzkKc9noWqQnU0IaMrtRgyOma", - "verificationMethod": "did:key:test", - "proofPurpose": "assertionMethod", - "created": "2021-10-01T18:16:41.072975+00:00", + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", }, - } + ] constraint = { "limit_disclosure": "required", "fields": [ { - "path": ["$.credentialSubject.Patient.address"], + "path": ["$.credentialSubject.Patient.address[0].city"], "purpose": "Test", } ], @@ -3288,6 +3320,26 @@ async def test_apply_constraint_received_cred_valid(self, profile): constraint=constraint, cred_dict=cred_dict ) + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_apply_constraint_received_cred_no_sel_disc(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_dict = deepcopy(TEST_CRED_DICT) + constraint = { + "fields": [ + { + "path": ["$.credentialSubject.Patient.address.country"], + "purpose": "Test", + } + ], + } + constraint = Constraints.deserialize(constraint) + assert not await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) + def test_get_field_updated_path(self, profile): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type @@ -3295,9 +3347,10 @@ def test_get_field_updated_path(self, profile): original_field = DIFField.deserialize( {"path": ["$.credentialSubject.Patient.address[0].city"], "purpose": "Test"} ) - updated_field = dif_pres_exch_handler.get_field_updated_path(original_field) - assert "$.credentialSubject.Patient.address.city" in updated_field.paths - assert "$.credentialSubject.Patient.address[0].city" not in updated_field.paths + field_a, field_b = dif_pres_exch_handler.get_field_updated_path(original_field) + assert field_a == original_field + assert "$.credentialSubject.Patient.address.city" in field_b.paths + assert "$.credentialSubject.Patient.address[0].city" not in field_b.paths original_field = DIFField.deserialize( { "path": [ @@ -3307,8 +3360,20 @@ def test_get_field_updated_path(self, profile): "purpose": "Test", } ) - updated_field = dif_pres_exch_handler.get_field_updated_path(original_field) + field_a, field_b = dif_pres_exch_handler.get_field_updated_path(original_field) + assert field_a == original_field + assert "$.credentialSubject.Patient.address[0].city" not in field_b.paths + assert "$.credentialSubject.Patient.address.city" in field_b.paths + assert "$.credentialSubject.Patient.birthDate" in field_b.paths - assert "$.credentialSubject.Patient.address[0].city" not in updated_field.paths - assert "$.credentialSubject.Patient.address.city" in updated_field.paths - assert "$.credentialSubject.Patient.birthDate" in updated_field.paths + def test_get_dict_keys_from_path(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_dict = { + "id": "urn:bnid:_:c14n14", + "type": ["MedicalPass", "VerifiableCredential"], + "issuanceDate": "2021-09-27T12:40:03+02:00", + "issuer": "did:key:zUC7DVPRfshooBqmnT2LrMxabCUkRhyyUCu8xKvYRot5aeTLTpPxzZoMyFkMLgKHMPUzdEnJM1EqbxfQd466ed3QuEtUJr8iqKRVfJ4txBa3PRoASaup6fjVAkU9VdbDbs5et64", + } + assert dif_pres_exch_handler.get_dict_keys_from_path(cred_dict, "issuer") == [] From 2687ed89d95a73f4d8ca7b3a8d2bb28939996a78 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Tue, 5 Oct 2021 07:59:52 -0700 Subject: [PATCH 04/29] wildcard path reveal doc fix Signed-off-by: Shaanjot Gill --- .../present_proof/dif/pres_exch_handler.py | 12 +- .../present_proof/dif/tests/test_data.py | 770 ++++++++++++++++++ .../dif/tests/test_pres_exch_handler.py | 42 +- 3 files changed, 810 insertions(+), 14 deletions(-) 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 de0db781b0..edb8e5a867 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -515,6 +515,8 @@ def reveal_doc(self, credential_dict: dict, constraints: Constraints): unflatten_dict = {} for field in constraints._fields: for path in field.paths: + if "[*]" in path: + path = path.replace("[*]", "[0]") jsonpath = parse(path) match = jsonpath.find(credential_dict) if len(match) == 0: @@ -1328,15 +1330,14 @@ async def verify_received_pres( f"apply to the enclosed credential in {desc_map_item_path}" ) - def get_field_updated_path(self, field: DIFField) -> Tuple[DIFField, DIFField]: + def get_field_updated_path(self, field: DIFField) -> DIFField: """Update DIFField path to remove any [*] in case of limit_disclosure.""" - new_field = deepcopy(field) given_paths = field.paths new_paths = [] for path in given_paths: new_paths.append(re.sub(r"\[(\W+)\]|\[(\d+)\]", "", path)) - new_field.paths = new_paths - return (field, new_field) + field.paths = new_paths + return field async def apply_constraint_received_cred( self, constraint: Constraints, cred_dict: dict @@ -1348,7 +1349,8 @@ async def apply_constraint_received_cred( is_limit_disclosure = constraint.limit_disclosure == "required" for field in fields: if is_limit_disclosure: - field, new_field = self.get_field_updated_path(field) + new_field = deepcopy(field) + new_field = self.get_field_updated_path(new_field) original_field_filter = await self.filter_by_field(field, credential) new_field_filter = await self.filter_by_field(new_field, credential) if not original_field_filter and not new_field_filter: 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 30b9074366..2d9123beb5 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py @@ -1549,6 +1549,776 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): }, } +TEST_CRED_WILDCARD = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/bbs/v1", + { + "LabReport": { + "@id": "https://www.vdel.com/LabReport", + "@context": { + "description": "http://schema.org/description", + "identifier": "http://schema.org/identifier", + "name": "http://schema.org/name", + "image": "http://schema.org/image", + }, + } + }, + { + "Specimen": { + "@id": "http://hl7.org/fhir/Specimen", + "@context": [ + None, + "https://fhircat.org/fhir-r5/rdf-r5/contexts/specimen.context.jsonld", + ], + } + }, + { + "Observation": { + "@id": "http://hl7.org/fhir/Observation", + "@context": [ + None, + "https://fhircat.org/fhir-r5/rdf-r5/contexts/observation.context.jsonld", + ], + } + }, + { + "Organization": { + "@id": "http://hl7.org/fhir/Organization", + "@context": [ + None, + "https://fhircat.org/fhir-r5/rdf-r5/contexts/organization.context.jsonld", + ], + } + }, + { + "Practitioner": { + "@id": "http://hl7.org/fhir/Practitioner", + "@context": [ + None, + "https://fhircat.org/fhir-r5/rdf-r5/contexts/practitioner.context.jsonld", + ], + } + }, + { + "DiagnosticReport": { + "@id": "http://hl7.org/fhir/DiagnosticReport", + "@context": [ + None, + "https://fhircat.org/fhir-r5/rdf-r5/contexts/diagnosticreport.context.jsonld", + ], + } + }, + { + "PractitionerRole": { + "@id": "http://hl7.org/fhir/PractitionerRole", + "@context": [ + None, + "https://fhircat.org/fhir-r5/rdf-r5/contexts/practitionerrole.context.jsonld", + ], + } + }, + ], + "type": ["VerifiableCredential", "LabReport"], + "issuer": "did:key:zUC74FYQCzCbDpbVm9v1LVCc2RkxJY3XMdxV9UpsVaerTgEAAjpdWfE8WemccfdNhski3kHiXfLzPZW2wgsvSCkZFWV3zSNxQEqZoV8kVpwLtLzzpskRcskBB3M3DxaeBnDvK4H", + "issuanceDate": "2021-10-01T20:17:12+02:00", + "credentialSubject": { + "DiagnosticReport": { + "resourceType": "http://hl7.org/fhir/resource-types#DiagnosticReport", + "id": "ca66dd0a-6ad5-db5e-e053-5a18000aa066", + "language": "sr-Cyrl-RS", + "basedOn": [ + {"reference": "ServiceRequest/ca66a8f6-1b0e-2881-e053-5a18000acec9"} + ], + "status": "final", + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0074", + "code": "LAB", + } + ] + }, + "code": { + "coding": [ + { + "system": "http://loinc.org", + "code": "11502-2", + "display": "Laboratory report", + } + ] + }, + "subject": {"reference": "Patient/ca66572a-0a1b-0d53-e053-5a18000ad0b7"}, + "effectiveDateTime": "2021-08-25T19:47:00EUROPE/BELGRADE", + "issued": "2021-08-25T19:47:00EUROPE/BELGRADE", + "performer": [ + {"reference": "PractitionerRole/ca6632d5-a447-6306-e053-5a18000a3953"} + ], + "specimen": [ + {"reference": "Specimen/ca666dfb-5a85-614a-e053-5a18000af20b"} + ], + "result": [ + {"reference": "Observation/ca708651-e8eb-3513-e053-5a18000ae79b"}, + {"reference": "Observation/ca708651-e8ec-3513-e053-5a18000ae79b"}, + {"reference": "Observation/ca708651-e8ed-3513-e053-5a18000ae79b"}, + {"reference": "Observation/ca708651-e8e9-3513-e053-5a18000ae79b"}, + {"reference": "Observation/ca708651-e8ea-3513-e053-5a18000ae79b"}, + {"reference": "Observation/ca708651-e8e7-3513-e053-5a18000ae79b"}, + {"reference": "Observation/ca708651-e8e8-3513-e053-5a18000ae79b"}, + ], + }, + "Specimen": { + "resourceType": "http://hl7.org/fhir/resource-types#Specimen", + "id": "ca666dfb-5a85-614a-e053-5a18000af20b", + "language": "sr-Cyrl-RS", + "accessionIdentifier": { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "ACSN", + } + ], + "text": "Broj protokola", + }, + "value": "124", + }, + "status": "available", + "type": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "122592007", + "display": "Acellular blood (serum or plasma) specimen", + } + ] + }, + "subject": {"reference": "Patient/ca66572a-0a1b-0d53-e053-5a18000ad0b7"}, + "receivedTime": "2021-08-25T19:16:00EUROPE/BELGRADE", + }, + "PractitionerRole": { + "resourceType": "http://hl7.org/fhir/resource-types#PractitionerRole", + "id": "ca6632d5-a447-6306-e053-5a18000a3953", + "active": True, + "practitioner": { + "reference": "Practitioner/ca5ed67e-5780-0136-e053-5a18000ae501" + }, + "organization": { + "reference": "Organization/ca661f4d-ffc6-6111-e053-5a18000a3dea" + }, + "code": [ + { + "coding": [ + { + "system": "http://hl7.org/fhir/uv/ips/ValueSet/healthcare-professional-roles-uv-ips", + "code": "2212", + "display": "Специјалисти лекари", + } + ] + } + ], + "specialty": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "408454008", + "display": "Клиничка микробиологија", + } + ] + } + ], + }, + "Practitioner": { + "resourceType": "http://hl7.org/fhir/resource-types#Practitioner", + "id": "ca5ed67e-5780-0136-e053-5a18000ae501", + "language": "sr-Cyrl-RS", + "text": "специјалиста медицинске микробиологије", + "active": True, + "name": [{"family": "Банчевић", "given": ["Маја"], "suffix": ["др"]}], + "gender": "female", + "qualification": [{"code": {"coding": [{"code": "MD"}]}}], + }, + "Observation": [ + { + "resourceType": "http://hl7.org/fhir/resource-types#Observation", + "id": "ca708651-e8eb-3513-e053-5a18000ae79b", + "language": "sr-Cyrl-RS", + "text": {"status": "generated", "div": "Negativan"}, + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory", + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/uv/ips/ValueSet/results-laboratory-observations-uv-ips", + "code": "24115-8", + "display": "Epstein Barr virus (EBV) IgM", + }, + { + "system": "http://snomed.info/sct", + "code": "40168006", + "display": "Epstein Barr virus (EBV)", + }, + { + "system": "http://snomed.info/sct", + "code": "74889000", + "display": "IgM", + }, + ] + }, + "subject": { + "reference": "Patient/ca66572a-0a1b-0d53-e053-5a18000ad0b7" + }, + "effectiveDateTime": "2021-08-26T07:09:00EUROPE/BELGRADE", + "method": { + "coding": [ + { + "system": "http://hl7.org/fhir/ValueSet/observation-methods", + "code": "76978006", + "display": "ELISA", + } + ] + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "2667000", + "display": "Odsutno", + } + ] + }, + "interpretation": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", + "code": "NEG", + "display": "Negativan", + } + ] + } + ], + }, + { + "resourceType": "http://hl7.org/fhir/resource-types#Observation", + "id": "ca708651-e8e8-3513-e053-5a18000ae79b", + "language": "sr-Cyrl-RS", + "text": {"status": "generated", "div": "Pozitivan"}, + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory", + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/uv/ips/ValueSet/results-laboratory-observations-uv-ips", + "code": "35275-7", + "display": "Morbille virus IgG", + }, + { + "system": "http://snomed.info/sct", + "code": "52584002", + "display": "Morbilli virus", + }, + { + "system": "http://snomed.info/sct", + "code": "29246005", + "display": "IgG", + }, + ] + }, + "subject": { + "reference": "Patient/ca66572a-0a1b-0d53-e053-5a18000ad0b7" + }, + "effectiveDateTime": "2021-08-26T07:09:00EUROPE/BELGRADE", + "method": { + "coding": [ + { + "system": "http://hl7.org/fhir/ValueSet/observation-methods", + "code": "76978006", + "display": "ELISA", + } + ] + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "52101004", + "display": "Prisutno", + } + ] + }, + "interpretation": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", + "code": "POS", + "display": "Pozitivan", + } + ] + } + ], + }, + { + "resourceType": "http://hl7.org/fhir/resource-types#Observation", + "id": "ca708651-e8e7-3513-e053-5a18000ae79b", + "language": "sr-Cyrl-RS", + "text": {"status": "generated", "div": "Negativan"}, + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory", + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/uv/ips/ValueSet/results-laboratory-observations-uv-ips", + "code": "35276-5", + "display": "Morbille virus IgM", + }, + { + "system": "http://snomed.info/sct", + "code": "52584002", + "display": "Morbilli virus", + }, + { + "system": "http://snomed.info/sct", + "code": "74889000", + "display": "IgM", + }, + ] + }, + "subject": { + "reference": "Patient/ca66572a-0a1b-0d53-e053-5a18000ad0b7" + }, + "effectiveDateTime": "2021-08-26T07:09:00EUROPE/BELGRADE", + "method": { + "coding": [ + { + "system": "http://hl7.org/fhir/ValueSet/observation-methods", + "code": "76978006", + "display": "ELISA", + } + ] + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "2667000", + "display": "Odsutno", + } + ] + }, + "interpretation": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", + "code": "NEG", + "display": "Negativan", + } + ] + } + ], + }, + { + "resourceType": "http://hl7.org/fhir/resource-types#Observation", + "id": "ca708651-e8ea-3513-e053-5a18000ae79b", + "language": "sr-Cyrl-RS", + "text": {"status": "generated", "div": "Pozitivan"}, + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory", + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/uv/ips/ValueSet/results-laboratory-observations-uv-ips", + "code": "29660-8", + "display": "Humani Parvo virus B19 IgG", + }, + { + "system": "http://snomed.info/sct", + "code": "63603005", + "display": "Humani Parvo virus B19", + }, + { + "system": "http://snomed.info/sct", + "code": "29246005", + "display": "IgG", + }, + ] + }, + "subject": { + "reference": "Patient/ca66572a-0a1b-0d53-e053-5a18000ad0b7" + }, + "effectiveDateTime": "2021-08-26T07:09:00EUROPE/BELGRADE", + "method": { + "coding": [ + { + "system": "http://hl7.org/fhir/ValueSet/observation-methods", + "code": "76978006", + "display": "ELISA", + } + ] + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "52101004", + "display": "Prisutno", + } + ] + }, + "interpretation": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", + "code": "POS", + "display": "Pozitivan", + } + ] + } + ], + }, + { + "resourceType": "http://hl7.org/fhir/resource-types#Observation", + "id": "ca708651-e8e9-3513-e053-5a18000ae79b", + "language": "sr-Cyrl-RS", + "text": {"status": "generated", "div": "Negativan"}, + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory", + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/uv/ips/ValueSet/results-laboratory-observations-uv-ips", + "code": "40658-7", + "display": "Humani Parvo virus B19 IgM", + }, + { + "system": "http://snomed.info/sct", + "code": "63603005", + "display": "Humani Parvo virus B19", + }, + { + "system": "http://snomed.info/sct", + "code": "74889000", + "display": "IgM", + }, + ] + }, + "subject": { + "reference": "Patient/ca66572a-0a1b-0d53-e053-5a18000ad0b7" + }, + "effectiveDateTime": "2021-08-26T07:09:00EUROPE/BELGRADE", + "method": { + "coding": [ + { + "system": "http://hl7.org/fhir/ValueSet/observation-methods", + "code": "76978006", + "display": "ELISA", + } + ] + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "2667000", + "display": "Odsutno", + } + ] + }, + "interpretation": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", + "code": "NEG", + "display": "Negativan", + } + ] + } + ], + }, + { + "resourceType": "http://hl7.org/fhir/resource-types#Observation", + "id": "ca708651-e8ed-3513-e053-5a18000ae79b", + "language": "sr-Cyrl-RS", + "text": {"status": "generated", "div": "Negativan"}, + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory", + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/uv/ips/ValueSet/results-laboratory-observations-uv-ips", + "code": "40729-6", + "display": "Herpes Simplex virus (HSV) IgM", + }, + { + "system": "http://snomed.info/sct", + "code": "19965007", + "display": "Herpes Simplex virus (HSV)", + }, + { + "system": "http://snomed.info/sct", + "code": "74889000", + "display": "IgM", + }, + ] + }, + "subject": { + "reference": "Patient/ca66572a-0a1b-0d53-e053-5a18000ad0b7" + }, + "effectiveDateTime": "2021-08-26T07:09:00EUROPE/BELGRADE", + "method": { + "coding": [ + { + "system": "http://hl7.org/fhir/ValueSet/observation-methods", + "code": "76978006", + "display": "ELISA", + } + ] + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "2667000", + "display": "Odsutno", + } + ] + }, + "interpretation": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", + "code": "NEG", + "display": "Negativan", + } + ] + } + ], + }, + { + "resourceType": "http://hl7.org/fhir/resource-types#Observation", + "id": "ca708651-e8ec-3513-e053-5a18000ae79b", + "language": "sr-Cyrl-RS", + "text": {"status": "generated", "div": "Pozitivan"}, + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory", + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/uv/ips/ValueSet/results-laboratory-observations-uv-ips", + "code": "24114-1", + "display": "Epstein Barr virus (EBV) IgG", + }, + { + "system": "http://snomed.info/sct", + "code": "40168006", + "display": "Epstein Barr virus (EBV)", + }, + { + "system": "http://snomed.info/sct", + "code": "29246005", + "display": "IgG", + }, + ] + }, + "subject": { + "reference": "Patient/ca66572a-0a1b-0d53-e053-5a18000ad0b7" + }, + "effectiveDateTime": "2021-08-26T07:09:00EUROPE/BELGRADE", + "method": { + "coding": [ + { + "system": "http://hl7.org/fhir/ValueSet/observation-methods", + "code": "76978006", + "display": "ELISA", + } + ] + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "52101004", + "display": "Prisutno", + } + ] + }, + "interpretation": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", + "code": "POS", + "display": "Pozitivan", + } + ] + } + ], + }, + ], + "Organization": [ + { + "resourceType": "http://hl7.org/fhir/resource-types#Organization", + "id": "ca661f4d-ffc6-6111-e053-5a18000a3dea", + "language": "sr-Cyrl-RS", + "active": True, + "type": ["team"], + "name": "Национална лабораторија за полиомијелитис и ентеровирусе", + "partOf": { + "reference": "Organization/ca661f4d-ffc5-6111-e053-5a18000a3dea", + "type": "Organization", + }, + "address": [{"type": "both", "country": "RS"}], + }, + { + "resourceType": "http://hl7.org/fhir/resource-types#Organization", + "id": "ca661f4d-ffc5-6111-e053-5a18000a3dea", + "language": "sr-Cyrl-RS", + "active": True, + "type": ["team"], + "name": "Одсек за серодијагностику и молекуларну дијагностику", + "partOf": { + "reference": "Organization/ca661c1e-cab1-611d-e053-5a18000af938", + "type": "Organization", + }, + "address": [{"type": "both", "country": "RS"}], + }, + { + "resourceType": "http://hl7.org/fhir/resource-types#Organization", + "id": "ca661c1e-cab1-611d-e053-5a18000af938", + "language": "sr-Cyrl-RS", + "active": True, + "type": ["team"], + "name": "Служба за лабораторијску дијагностику", + "partOf": { + "reference": "Organization/ca65fdc3-3516-4830-e053-5a18000af96e", + "type": "Organization", + }, + "address": [{"type": "both", "country": "RS"}], + }, + { + "resourceType": "http://hl7.org/fhir/resource-types#Organization", + "id": "ca65fdc3-3516-4830-e053-5a18000af96e", + "language": "sr-Cyrl-RS", + "identifier": [ + { + "use": "official", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "XX", + } + ], + "text": "Matični broj", + }, + "system": "http://www.apr.gov.rs/регистри/здравствене-установе", + "value": "17078712", + }, + { + "use": "official", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "TAX", + } + ], + "text": "PIB", + }, + "system": "http://www.purs.gov.rs/pib.html", + "value": "101739057", + }, + ], + "active": True, + "type": ["prov"], + "name": 'ИНСТИТУТ ЗА ВИРУСОЛОГИЈУ, ВАКЦИНЕ И СЕРУМЕ "ТОРЛАК"', + "telecom": [ + {"system": "email", "value": "office@torlak.rs", "rank": 1}, + {"system": "phone", "value": "+381113953700", "rank": 3}, + ], + "address": [ + { + "type": "both", + "line": ["Војводе Степе 458"], + "city": "Београд", + "country": "RS", + } + ], + }, + ], + }, + "name": "VDEL Lab Report", +} + def get_test_data(): vc_record_list = [] 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 db6e286010..c2d2b105e4 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 @@ -52,6 +52,7 @@ is_holder_pd_multiple_fields_excluded, is_holder_pd_multiple_fields_included, TEST_CRED_DICT, + TEST_CRED_WILDCARD, ) @@ -641,6 +642,27 @@ async def test_reveal_doc_c(self, setup_tuple, profile): ) assert tmp_reveal_doc + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_reveal_doc_wildcard(self, profile): + dif_pres_exch_handler = DIFPresExchHandler(profile) + test_constraint = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Observation[*].effectiveDateTime"], + "id": "Observation_effectiveDateTime", + "purpose": "Време узимања узорка", + } + ], + } + + test_constraint = Constraints.deserialize(test_constraint) + tmp_reveal_doc = dif_pres_exch_handler.reveal_doc( + credential_dict=TEST_CRED_WILDCARD, constraints=test_constraint + ) + assert tmp_reveal_doc + @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures async def test_filter_number_type_check(self, profile): @@ -3347,10 +3369,11 @@ def test_get_field_updated_path(self, profile): original_field = DIFField.deserialize( {"path": ["$.credentialSubject.Patient.address[0].city"], "purpose": "Test"} ) - field_a, field_b = dif_pres_exch_handler.get_field_updated_path(original_field) - assert field_a == original_field - assert "$.credentialSubject.Patient.address.city" in field_b.paths - assert "$.credentialSubject.Patient.address[0].city" not in field_b.paths + return_field = dif_pres_exch_handler.get_field_updated_path( + deepcopy(original_field) + ) + assert "$.credentialSubject.Patient.address.city" in return_field.paths + assert "$.credentialSubject.Patient.address[0].city" not in return_field.paths original_field = DIFField.deserialize( { "path": [ @@ -3360,11 +3383,12 @@ def test_get_field_updated_path(self, profile): "purpose": "Test", } ) - field_a, field_b = dif_pres_exch_handler.get_field_updated_path(original_field) - assert field_a == original_field - assert "$.credentialSubject.Patient.address[0].city" not in field_b.paths - assert "$.credentialSubject.Patient.address.city" in field_b.paths - assert "$.credentialSubject.Patient.birthDate" in field_b.paths + return_field = dif_pres_exch_handler.get_field_updated_path( + deepcopy(original_field) + ) + assert "$.credentialSubject.Patient.address[0].city" not in return_field.paths + assert "$.credentialSubject.Patient.address.city" in return_field.paths + assert "$.credentialSubject.Patient.birthDate" in return_field.paths def test_get_dict_keys_from_path(self, profile): dif_pres_exch_handler = DIFPresExchHandler( From 8d8723a66be20113246ccf339d088a5244d4f13d Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Tue, 5 Oct 2021 08:58:28 -0700 Subject: [PATCH 05/29] reveal doc frame input parameter Signed-off-by: Shaanjot Gill --- .../present_proof/dif/pres_exch_handler.py | 93 +++++++++--------- .../present_proof/dif/pres_request_schema.py | 26 +++++ .../dif/tests/test_pres_exch_handler.py | 53 +++++++++++ .../present_proof/v2_0/formats/dif/handler.py | 7 +- .../v2_0/formats/dif/tests/test_handler.py | 94 +++++++++++++++++++ open-api/openapi.json | 23 +++++ 6 files changed, 251 insertions(+), 45 deletions(-) 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 edb8e5a867..96635a922f 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -92,6 +92,7 @@ def __init__( profile: Profile, pres_signing_did: str = None, proof_type: str = None, + reveal_doc: dict = None, ): """Initialize PresExchange Handler.""" super().__init__() @@ -102,6 +103,7 @@ def __init__( else: self.proof_type = proof_type self.is_holder = False + self.reveal_doc_frame = reveal_doc async def _get_issue_suite( self, @@ -504,50 +506,53 @@ def create_vcrecord(self, cred_dict: dict) -> VCRecord: def reveal_doc(self, credential_dict: dict, constraints: Constraints): """Generate reveal_doc dict for deriving credential.""" - derived = { - "@context": credential_dict.get("@context"), - "type": credential_dict.get("type"), - "@explicit": True, - "@requireAll": True, - "issuanceDate": {}, - "issuer": {}, - } - unflatten_dict = {} - for field in constraints._fields: - for path in field.paths: - if "[*]" in path: - path = path.replace("[*]", "[0]") - jsonpath = parse(path) - match = jsonpath.find(credential_dict) - if len(match) == 0: - continue - for match_item in match: - full_path = str(match_item.full_path) - if bool(re.search(pattern=r"\[[0-9]+\]", string=full_path)): - full_path = full_path.replace(".[", "[") - unflatten_dict[full_path] = {} - explicit_key_path = None - key_list = full_path.split(".")[:-1] - for key in key_list: - if not explicit_key_path: - explicit_key_path = key - else: - explicit_key_path = explicit_key_path + "." + key - unflatten_dict[explicit_key_path + ".@explicit"] = True - unflatten_dict[explicit_key_path + ".@requireAll"] = True - derived = self.new_credential_builder(derived, unflatten_dict) - # Fix issue related to credentialSubject type property - if "credentialSubject" in derived.keys(): - if "type" in credential_dict.get("credentialSubject"): - derived["credentialSubject"]["type"] = credential_dict.get( - "credentialSubject" - ).get("type") - if "credentialSubject" not in derived.keys(): - if isinstance(credential_dict.get("credentialSubject"), list): - derived["credentialSubject"] = [] - elif isinstance(credential_dict.get("credentialSubject"), dict): - derived["credentialSubject"] = {} - return derived + if not self.reveal_doc_frame: + derived = { + "@context": credential_dict.get("@context"), + "type": credential_dict.get("type"), + "@explicit": True, + "@requireAll": True, + "issuanceDate": {}, + "issuer": {}, + } + unflatten_dict = {} + for field in constraints._fields: + for path in field.paths: + if "[*]" in path: + path = path.replace("[*]", "[0]") + jsonpath = parse(path) + match = jsonpath.find(credential_dict) + if len(match) == 0: + continue + for match_item in match: + full_path = str(match_item.full_path) + if bool(re.search(pattern=r"\[[0-9]+\]", string=full_path)): + full_path = full_path.replace(".[", "[") + unflatten_dict[full_path] = {} + explicit_key_path = None + key_list = full_path.split(".")[:-1] + for key in key_list: + if not explicit_key_path: + explicit_key_path = key + else: + explicit_key_path = explicit_key_path + "." + key + unflatten_dict[explicit_key_path + ".@explicit"] = True + unflatten_dict[explicit_key_path + ".@requireAll"] = True + derived = self.new_credential_builder(derived, unflatten_dict) + # Fix issue related to credentialSubject type property + if "credentialSubject" in derived.keys(): + if "type" in credential_dict.get("credentialSubject"): + derived["credentialSubject"]["type"] = credential_dict.get( + "credentialSubject" + ).get("type") + if "credentialSubject" not in derived.keys(): + if isinstance(credential_dict.get("credentialSubject"), list): + derived["credentialSubject"] = [] + elif isinstance(credential_dict.get("credentialSubject"), dict): + derived["credentialSubject"] = {} + return derived + else: + return self.reveal_doc_frame def new_credential_builder( self, new_credential: dict, unflatten_dict: dict diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_request_schema.py b/aries_cloudagent/protocols/present_proof/dif/pres_request_schema.py index a318c8c4be..e241a48d40 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_request_schema.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_request_schema.py @@ -50,3 +50,29 @@ class DIFPresSpecSchema(OpenAPISchema): PresentationDefinitionSchema(), required=False, ) + reveal_doc = fields.Dict( + description=( + "reveal doc [JSON-LD frame] dict used" + " to derive the credential when selective" + " disclosure is required" + ), + required=False, + example={ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/bbs/v1", + ], + "type": ["VerifiableCredential", "LabReport"], + "@explicit": True, + "@requireAll": True, + "issuanceDate": {}, + "issuer": {}, + "credentialSubject": { + "Observation": [ + {"effectiveDateTime": {}, "@explicit": True, "@requireAll": True} + ], + "@explicit": True, + "@requireAll": True, + }, + }, + ) 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 c2d2b105e4..dd64a1a5d3 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 @@ -496,6 +496,59 @@ async def test_limit_disclosure_required_check(self, setup_tuple, profile): ] assert cred["proof"]["type"] == "BbsBlsSignatureProof2020" + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_reveal_doc_with_frame_provided(self, profile): + reveal_doc_frame = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/bbs/v1", + ], + "type": ["VerifiableCredential", "LabReport"], + "@explicit": True, + "@requireAll": True, + "issuanceDate": {}, + "issuer": {}, + "credentialSubject": { + "Observation": [ + {"effectiveDateTime": {}, "@explicit": True, "@requireAll": True} + ], + "@explicit": True, + "@requireAll": True, + }, + } + dif_pres_exch_handler = DIFPresExchHandler(profile, reveal_doc=reveal_doc_frame) + test_constraint = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.givenName"], + "filter": {"type": "string", "const": "JOHN"}, + }, + { + "path": ["$.credentialSubject.familyName"], + "filter": {"type": "string", "const": "SMITH"}, + }, + { + "path": ["$.credentialSubject.type"], + "filter": { + "type": "string", + "enum": ["PermanentResident", "Person"], + }, + }, + { + "path": ["$.credentialSubject.gender"], + "filter": {"type": "string", "const": "Male"}, + }, + ], + } + + test_constraint = Constraints.deserialize(test_constraint) + tmp_reveal_doc = dif_pres_exch_handler.reveal_doc( + credential_dict=BBS_SIGNED_VC_MATTR, constraints=test_constraint + ) + assert tmp_reveal_doc == reveal_doc_frame + @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures async def test_reveal_doc_a(self, profile): diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py index 937ee4acd0..a51bed992d 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py @@ -164,6 +164,7 @@ async def create_pres( ) pres_definition = None limit_record_ids = None + reveal_doc_frame = None challenge = None domain = None if request_data != {} and DIFPresFormatHandler.format.api in request_data: @@ -173,6 +174,7 @@ async def create_pres( pres_definition = pres_spec_payload.get("presentation_definition") issuer_id = pres_spec_payload.get("issuer_id") limit_record_ids = pres_spec_payload.get("record_ids") + reveal_doc_frame = pres_spec_payload.get("reveal_doc") if not pres_definition: if "options" in proof_request: challenge = proof_request.get("options").get("challenge") @@ -303,7 +305,10 @@ async def create_pres( raise V20PresFormatHandlerError(err) dif_handler = DIFPresExchHandler( - self._profile, pres_signing_did=issuer_id, proof_type=dif_handler_proof_type + self._profile, + pres_signing_did=issuer_id, + proof_type=dif_handler_proof_type, + reveal_doc=reveal_doc_frame, ) pres = await dif_handler.create_vp( diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py index 39676cc261..3ca7bc6a4f 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py @@ -524,6 +524,100 @@ async def test_create_pres_prover_proof_spec_with_record_ids(self): ) assert output[1].data.json_ == DIF_PRES + async def test_create_pres_prover_proof_spec_with_reveal_doc(self): + dif_pres_spec = deepcopy(DIF_PRES_REQUEST_A) + dif_pres_spec["issuer_id"] = "test123" + dif_pres_spec["reveal_doc"] = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/bbs/v1", + ], + "type": ["VerifiableCredential", "LabReport"], + "@explicit": True, + "@requireAll": True, + "issuanceDate": {}, + "issuer": {}, + "credentialSubject": { + "Observation": [ + {"effectiveDateTime": {}, "@explicit": True, "@requireAll": True} + ], + "@explicit": True, + "@requireAll": True, + }, + } + 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="did:example:489398593", + subject_ids=[ + "did:sov:WgWxqztrNooG92RXvxSTWv", + ], + proof_types=["Ed25519Signature2018"], + schema_ids=["https://example.org/examples/degree.json"], + cred_value={"...", "..."}, + given_id="http://example.edu/credentials/3732", + cred_tags={"some": "tag"}, + record_id="test1", + ) + ] + dif_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_REQUEST_B, ident="dif") + ], + ) + record = V20PresExRecord( + pres_ex_id="pxid", + thread_id="thid", + connection_id="conn_id", + initiator="init", + role="role", + state="state", + pres_request=dif_pres_request, + verified="false", + auto_present=True, + error_msg="error", + ) + request_data = {} + request_data["dif"] = dif_pres_spec + + self.context.injector.bind_instance( + VCHolder, + async_mock.MagicMock( + search_credentials=async_mock.MagicMock( + return_value=async_mock.MagicMock( + fetch=async_mock.CoroutineMock(return_value=cred_list) + ) + ) + ), + ) + + with async_mock.patch.object( + DIFPresExchHandler, + "create_vp", + async_mock.CoroutineMock(), + ) as mock_create_vp: + mock_create_vp.return_value = DIF_PRES + output = await self.handler.create_pres(record, request_data) + assert isinstance(output[0], V20PresFormat) and isinstance( + output[1], AttachDecorator + ) + assert output[1].data.json_ == DIF_PRES + async def test_create_pres_no_challenge(self): dif_pres_req = deepcopy(DIF_PRES_REQUEST_B) del dif_pres_req["options"]["challenge"] diff --git a/open-api/openapi.json b/open-api/openapi.json index b63c0078ac..6e89d09b91 100644 --- a/open-api/openapi.json +++ b/open-api/openapi.json @@ -6044,6 +6044,29 @@ }, "description" : "Mapping of input_descriptor id to list of stored W3C credential record_id", "properties" : { } + }, + "reveal_doc" : { + "type" : "object", + "example" : { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/bbs/v1" + ], + "type": ["VerifiableCredential", "LabReport"], + "@explicit": true, + "@requireAll": true, + "issuanceDate": {}, + "issuer": {}, + "credentialSubject": { + "Observation": [ + {"effectiveDateTime": {}, "@explicit": true, "@requireAll": true} + ], + "@explicit": true, + "@requireAll": true + } + }, + "description" : "reveal doc [JSON-LD frame] dict used to derive the credential when selective disclosure is required", + "properties" : { } } } }, From 3dc1b823a6b04aed381855692251740579d2c7b0 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Tue, 5 Oct 2021 10:20:24 -0700 Subject: [PATCH 06/29] reveal doc fix [non credentialSubject path] Signed-off-by: Shaanjot Gill --- .../protocols/present_proof/dif/pres_exch_handler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 96635a922f..ebca960153 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -549,7 +549,10 @@ def reveal_doc(self, credential_dict: dict, constraints: Constraints): if isinstance(credential_dict.get("credentialSubject"), list): derived["credentialSubject"] = [] elif isinstance(credential_dict.get("credentialSubject"), dict): - derived["credentialSubject"] = {} + derived["credentialSubject"] = { + "@explicit": True, + "@requireAll": True, + } return derived else: return self.reveal_doc_frame From f23aa6e0bc2f7159fd4e92e3cad525c54b3e617e Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Tue, 5 Oct 2021 12:38:16 -0700 Subject: [PATCH 07/29] workaround for @embed and exclusion flags not working inside credentialSubject Signed-off-by: Shaanjot Gill --- .../protocols/present_proof/dif/pres_exch_handler.py | 8 -------- 1 file changed, 8 deletions(-) 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 ebca960153..602cf2cb92 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -545,14 +545,6 @@ def reveal_doc(self, credential_dict: dict, constraints: Constraints): derived["credentialSubject"]["type"] = credential_dict.get( "credentialSubject" ).get("type") - if "credentialSubject" not in derived.keys(): - if isinstance(credential_dict.get("credentialSubject"), list): - derived["credentialSubject"] = [] - elif isinstance(credential_dict.get("credentialSubject"), dict): - derived["credentialSubject"] = { - "@explicit": True, - "@requireAll": True, - } return derived else: return self.reveal_doc_frame From 9b59aa3c5babb0d794b28fe1cac980df6f6ec7f6 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Wed, 6 Oct 2021 22:48:10 -0700 Subject: [PATCH 08/29] verification refactor and updates - pending testing Signed-off-by: Shaanjot Gill --- .../present_proof/dif/pres_exch_handler.py | 84 +++--- .../dif/tests/test_pres_exch_handler.py | 273 ++++++++++++------ .../v2_0/formats/dif/tests/test_handler.py | 157 ++++++++++ 3 files changed, 389 insertions(+), 125 deletions(-) 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 602cf2cb92..0771cf2861 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -11,7 +11,6 @@ import pytz import re -from copy import deepcopy from datetime import datetime from dateutil.parser import parse as dateutil_parser from dateutil.parser import ParserError @@ -1330,15 +1329,6 @@ async def verify_received_pres( f"apply to the enclosed credential in {desc_map_item_path}" ) - def get_field_updated_path(self, field: DIFField) -> DIFField: - """Update DIFField path to remove any [*] in case of limit_disclosure.""" - given_paths = field.paths - new_paths = [] - for path in given_paths: - new_paths.append(re.sub(r"\[(\W+)\]|\[(\d+)\]", "", path)) - field.paths = new_paths - return field - async def apply_constraint_received_cred( self, constraint: Constraints, cred_dict: dict ) -> bool: @@ -1349,17 +1339,9 @@ async def apply_constraint_received_cred( is_limit_disclosure = constraint.limit_disclosure == "required" for field in fields: if is_limit_disclosure: - new_field = deepcopy(field) - new_field = self.get_field_updated_path(new_field) - original_field_filter = await self.filter_by_field(field, credential) - new_field_filter = await self.filter_by_field(new_field, credential) - if not original_field_filter and not new_field_filter: - return False - elif not original_field_filter and new_field_filter: - field = new_field - else: - if not await self.filter_by_field(field, credential): - return False + field = await self.get_updated_field(field, cred_dict) + if not await self.filter_by_field(field, credential): + return False field_paths = field_paths + field.paths # Selective Disclosure check if is_limit_disclosure: @@ -1379,7 +1361,6 @@ async def apply_constraint_received_cred( if field_path.count(".") >= 1: split_field_path = field_path.split(".") key = ".".join(split_field_path[:-1]) - key = re.sub(r"\[(\W+)\]|\[(\d+)\]", "", key) value = split_field_path[-1] nested_field_paths = self.build_nested_paths_dict( key, value, nested_field_paths, cred_dict @@ -1395,8 +1376,7 @@ async def apply_constraint_received_cred( return False for nested_attr_key in nested_field_paths: nested_attr_values = nested_field_paths[nested_attr_key] - split_nested_attr_key = nested_attr_key.split(".") - extracted = self.nested_get(cred_dict, split_nested_attr_key) + extracted = self.nested_get(cred_dict, nested_attr_key) if isinstance(extracted, dict): if not self.check_attr_in_extracted_dict( extracted, nested_attr_values @@ -1419,13 +1399,11 @@ def check_attr_in_extracted_dict( return False return True - def get_dict_keys_from_path(self, derived_cred_dict: dict, path: str) -> list: - """Return list of keys as additional_attrs to build nested_field_paths.""" + def get_dict_keys_from_path(self, derived_cred_dict: dict, path: str) -> List: + """Return additional_attrs to build nested_field_paths.""" additional_attrs = [] mandatory_paths = ["@id", "@type"] - jsonpath = parse(path) - match = jsonpath.find(derived_cred_dict) - match_item = match[0].value + match_item = self.nested_get(derived_cred_dict, path) if isinstance(match_item, dict): for key in match_item.keys(): if key in mandatory_paths: @@ -1436,14 +1414,45 @@ def get_dict_keys_from_path(self, derived_cred_dict: dict, path: str) -> list: additional_attrs.append(key) return additional_attrs - def nested_get( - self, input_dict: dict, nested_key: Sequence[str] - ) -> Union[Dict, List]: + async def get_updated_field(self, field: DIFField, cred: dict) -> DIFField: + """Return field with updated json path, if necessary.""" + new_paths = [] + for path in field.paths: + new_paths.append(await self.get_updated_path(cred, path)) + field.paths = new_paths + return field + + async def get_updated_path(self, cred_dict: dict, json_path: str) -> str: + """Return updated json path, if necessary.""" + + def update_path_recursive_call(path: str, level: int = 0): + path_split_array = path.split(".", level) + to_check = path_split_array[-1] + if "." not in to_check: + return path + split_by_index = re.split(r"\[(\d+)\]", to_check, 1) + if len(split_by_index) > 1: + jsonpath = parse(split_by_index[0]) + match = jsonpath.find(cred_dict) + if len(match) > 0: + if isinstance(match[0].value, dict): + new_path = split_by_index[0] + split_by_index[2] + return update_path_recursive_call(new_path, level + 1) + return update_path_recursive_call(path, level + 1) + + return update_path_recursive_call(json_path) + + def nested_get(self, input_dict: dict, path: str) -> Union[Dict, List]: """Return dict or list from nested dict given list of nested_key.""" - internal_value = input_dict - for k in nested_key: - internal_value = internal_value.get(k, None) - return internal_value + jsonpath = parse(path) + match = jsonpath.find(input_dict) + if len(match) > 1: + return_list = [] + for match_item in match: + return_list.append(match_item.value) + return return_list + else: + return match[0].value def build_nested_paths_dict( self, @@ -1453,11 +1462,12 @@ def build_nested_paths_dict( cred_dict: dict, ) -> dict: """Build and return nested_field_paths dict.""" + additional_attrs = self.get_dict_keys_from_path(cred_dict, key) + value = re.sub(r"\[(\W+)\]|\[(\d+)\]", "", value) if key in nested_field_paths.keys(): nested_field_paths[key].add(value) else: nested_field_paths[key] = {value} - additional_attrs = self.get_dict_keys_from_path(cred_dict, key) if len(additional_attrs) > 0: for attr in additional_attrs: nested_field_paths[key].add(attr) 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 dd64a1a5d3..de7fa5cdc9 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 @@ -3228,73 +3228,38 @@ async def test_is_holder_missing_subject(self, profile, setup_tuple): @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures - async def test_apply_constraint_received_cred_invalid_a(self, profile): + async def test_apply_constraint_received_cred_path_update(self, profile): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type ) - cred_dict = { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/bbs/v1", - { - "MedicalPass": { - "@id": "https://www.vdel.com/MedicalPass", - "@context": { - "description": "http://schema.org/description", - "identifier": "http://schema.org/identifier", - "name": "http://schema.org/name", - "image": "http://schema.org/image", - }, - } - }, - { - "Patient": { - "@id": "http://hl7.org/fhir/Patient", - "@context": [ - "https://fhircat.org/fhir-r5/rdf-r5/contexts/patient.context.jsonld" - ], - } - }, - ], - "id": "urn:bnid:_:c14n14", - "type": ["MedicalPass", "VerifiableCredential"], - "credentialSubject": { - "id": "urn:bnid:_:c14n11", - "Patient": { - "@id": "urn:bnid:_:c14n2", - "type": "fhir:resource-types#Patient", - "birthDate": "1978-12-06T00:00:00", - }, + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"]["Patient"]["address"] = [ + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", }, - "issuanceDate": "2021-09-27T12:40:03+02:00", - "issuer": "did:key:test", - "proof": { - "type": "BbsBlsSignatureProof2020", - "nonce": "XVdQwHnUYJkkMX4LDWFPVuB7NQJ5IVn6ohW/psGv3fFSJ9kbr59BcWpix7Q3LBfzJ80=", - "proofValue": "ADgAAAAQAnwED6993W+P6/Iptfdk1uEaOcKDEYYJVjR154bB9r+skTZFR/P82oKJpMzZtMD+jRLV1Y4STy/JCg2nuKQVkyNbBTOxnJ5gNrYH7ZVC0IkQbq16Ld9bPkS7JeU2pfQ1fDZAL6CrkX+k7E24FFFe+ssLrkKLTDWe/J94+MJW/3qGAIuOKQ3t739ILPkElMHdNaxLNAAAAHSKc0EckEFyjM7bBqlxaLJhxkU2kcyR5ZwBee9UGzD+2s3J90rDvg4KUw7d4eli2SAAAAACGXrTBdx3eL+ki5OUBqpQ3k9oYMIwifYJdhtE4BfdMhgVwTWuNcXgXXvV46vOB1uJLi2/zaWzhAWDY9EGRM0nhot3dail73ts2KYPa+/pUMnWlmF5tVYzJNIQcPiIFzO27wA1mnFy7VXmbgHpGUmwqgAAAC5uHM39yBidgmkXY+zhOQJSC8CAJ1R2VLlOg+N8eOecIGSqaQGE7zTKrCTxlMsuOmP7tlxhdRgGIrzP8bhhbceTRlfIM4BDuQNkWs1LfYoeb5sUaNXKTgD802xQiHTMGMhncZfHM0l2pw07b0dTR4pU07EES+APYt5FoELvhn4BxB0Ci5qGNiX3Dv71i1ceSizpqgDR/hhbEVeuum2drdEkCF4TU6ZHO+/B4VawYEMAtJLRRFpmUWEF5EngsuaWd3hKfRFjMEEOA7Q2JrWxP2OGRLwzGN4eMalZTMYbkSp+QlJ1Q9xpzSR+y2tqocHXvVMBMaM5yjLem1tf2McsKiYIQfeKrYHQhJJK35pLSkd5qhl6SWOQrNET2iEgQm9hOL1zW7JBFk/05DuRQB8SYHbji5Cnqlj0urLTQvJWWsmkfBusleQVxX17AbFo9RbZnZumDCFJkOd++NpQS7cnpWUrJtxgX+iCX/dbyqU39ADJEtK77632tEptJSnqazMAnnEYhG0OnPYG40691iT7ojmWoxVvhyFGN+xbrCqTvNNMrlaf/JQN/wfP1ClmQqjdFUpKUMOfcoMJnU3s8NMcQlS3ABqKxscD/pPpOVs6u1ZlQnPdp3XYt3gUwhyFpUXRArUzkyk2G5dTJCC1k9jwfkP2Nz/AKk76DVGne10v7o5SqT/NniuaxexOgmUmjwZo1LQPELoJQ5944slUQavntnU4KtKvpkP235l9NjoxnMTB4gcW9quoqsgbmZ8s68rAJjUPiHDfIiKEuVZpzsv1nZQUkqqb1nov4zWWrBqpG607z2tV6MmpA+tlnwoytHjRYOEKzUNxMkbZHLIQMTGWdXnsRYrwkGepi2GiPmvNV0YeWnb08eW2EZ6KomGYhG/HqRBUwAk5O86cpellucE9Wk3aiYNAKPIImPvg6H8I9zxGzBJrFfKV1pZPMEPHHiQTOZ+388ZgGkNpbWxqJC1WFCwKc+qaqNngu4WJ1FhcbmhYYAhS00GWKqNlNuOYm/80p+83p3J50yzddBBOAP5nswXU/E8xFnHE445aj+fvHbEgF249vOTvFgEvctJoBsBdIK84IL1qzk94SYapqupzUHYZFvECZDuz0s5AgF4dM5UWzL37tZnjBK0uBMNLd0EmkKw++GYTy5PwFrrilEwiUSqbineurvJjbyEurBi0aytZEiOSsJwDw6jnvxOVfd2OFjmc70np4meKz1xrewyscto0Vkjw7k/9dOQTlRAwsTqAWkgyAAQK1jxP4VJYIxzUqA9krmWifViPNbRHzHIdpSC8ngkZXJE89/q6my4ADxfTRG3zy8ED2ZTrSJYsw8KA4SGqV3+MeRjAg9jaXFXxdNlpBE+TbpnS7kqMK035QyxCHej187uHbqfmS8cWdXTK6T5sMUdgdWw+TpEp3JaVS7fRRjED8O4VkVRUFs9EsWmL6CbuldGJYv1Gu9jISW3zeMkmQxPdOeC3HHqO14/gcHjIZZRv7RqWJDr4LDAjw6DDIfSM4KERrsvNuvkIsZLGmx5L6ZhL3BG/X0KJ2SF2iKkKn+/CHMha/oJwP0ob78GetwByVW2lRImccKjGM0daOS9n9OIGTtju6Mvj2/VYXemHGxXmcT/Q064/KlTi010n4+bAUsqDHJxgKCfADLCiiVFiI7W7mZldJ5URHJNORb6TM2p9YN8E45+7KWw+OUg2MVFpG53I0dWea7tS97GyGIr+scwkLSl5WT9Pct7zVeDTGT8yxlxBJkR6RBqcIxstbqAPofQJfXHTn/9NdXCLHrVIABUS9QcmRszlCyE/vt50bU5tnaM7ynda/t4fK/JYzlfA2ZkJSjdQMM3Ke0hhPAWTQEV4pH6lH5lzTFkdyHdnCt/Ck5654fbSR6KGqb8zXqZ+kW7w2pbULXf7bAcrOy5my7uxmo6GdusS1TG70Zh9dNH1fdsKH+xFHVYF5aofOQ==", - "verificationMethod": "did:key:test", - "proofPurpose": "assertionMethod", - "created": "2021-09-27T10:40:03.200843+00:00", + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", }, - } + ] constraint = { "limit_disclosure": "required", "fields": [ { - "path": ["$.credentialSubject.Patient.birthDate"], - "id": "birthDate", - "purpose": "test", + "path": ["$.credentialSubject.Patient[0].address[*].city"], + "purpose": "Test", } ], } constraint = Constraints.deserialize(constraint) - assert not await dif_pres_exch_handler.apply_constraint_received_cred( + assert await dif_pres_exch_handler.apply_constraint_received_cred( constraint=constraint, cred_dict=cred_dict ) @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures - async def test_apply_constraint_received_cred_invalid_b(self, profile): + async def test_apply_constraint_received_cred_invalid(self, profile): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type ) @@ -3303,16 +3268,32 @@ async def test_apply_constraint_received_cred_invalid_b(self, profile): { "@id": "urn:bnid:_:c14n1", "city": "Рума", + "country": "test", }, { - "country": "Рума", + "@id": "urn:bnid:_:c14n1", + "city": "Рума", }, ] constraint = { "limit_disclosure": "required", "fields": [ { - "path": ["$.credentialSubject.Patient.address[0].city"], + "path": ["$.credentialSubject.Patient[0].address[0].city[0]"], + "purpose": "Test", + } + ], + } + constraint = Constraints.deserialize(constraint) + assert not await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) + + constraint = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient[0].address[*].city"], "purpose": "Test", } ], @@ -3324,10 +3305,11 @@ async def test_apply_constraint_received_cred_invalid_b(self, profile): @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures - async def test_apply_constraint_received_cred_valid_a(self, profile): + async def test_apply_constraint_received_cred_valid(self, profile): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type ) + cred_dict = deepcopy(TEST_CRED_DICT) constraint = { "limit_disclosure": "required", @@ -3343,13 +3325,6 @@ async def test_apply_constraint_received_cred_valid_a(self, profile): constraint=constraint, cred_dict=cred_dict ) - @pytest.mark.asyncio - @pytest.mark.ursa_bbs_signatures - async def test_apply_constraint_received_cred_valid_b(self, profile): - dif_pres_exch_handler = DIFPresExchHandler( - profile, proof_type=BbsBlsSignature2020.signature_type - ) - cred_dict = deepcopy(TEST_CRED_DICT) constraint = { "limit_disclosure": "required", "fields": [ @@ -3364,13 +3339,6 @@ async def test_apply_constraint_received_cred_valid_b(self, profile): constraint=constraint, cred_dict=cred_dict ) - @pytest.mark.asyncio - @pytest.mark.ursa_bbs_signatures - async def test_apply_constraint_received_cred_valid_c(self, profile): - dif_pres_exch_handler = DIFPresExchHandler( - profile, proof_type=BbsBlsSignature2020.signature_type - ) - cred_dict = deepcopy(TEST_CRED_DICT) cred_dict["credentialSubject"]["Patient"]["address"] = [ { "@id": "urn:bnid:_:c14n1", @@ -3395,6 +3363,88 @@ async def test_apply_constraint_received_cred_valid_c(self, profile): constraint=constraint, cred_dict=cred_dict ) + cred_dict["credentialSubject"]["Patient"] = [ + { + "address": [ + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + ] + }, + { + "address": [ + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + ] + }, + ] + constraint = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient[0].address[0].city"], + "purpose": "Test", + } + ], + } + constraint = Constraints.deserialize(constraint) + assert await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) + + constraint = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient[*].address[*].city"], + "purpose": "Test", + } + ], + } + constraint = Constraints.deserialize(constraint) + assert await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) + + constraint = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient[*].address[0].city"], + "purpose": "Test", + } + ], + } + constraint = Constraints.deserialize(constraint) + assert await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) + + constraint = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient[0].address[*].city"], + "purpose": "Test", + } + ], + } + constraint = Constraints.deserialize(constraint) + assert await dif_pres_exch_handler.apply_constraint_received_cred( + constraint=constraint, cred_dict=cred_dict + ) + @pytest.mark.asyncio @pytest.mark.ursa_bbs_signatures async def test_apply_constraint_received_cred_no_sel_disc(self, profile): @@ -3415,33 +3465,36 @@ async def test_apply_constraint_received_cred_no_sel_disc(self, profile): constraint=constraint, cred_dict=cred_dict ) - def test_get_field_updated_path(self, profile): + @pytest.mark.asyncio + async def test_get_updated_path(self, profile): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type ) - original_field = DIFField.deserialize( - {"path": ["$.credentialSubject.Patient.address[0].city"], "purpose": "Test"} - ) - return_field = dif_pres_exch_handler.get_field_updated_path( - deepcopy(original_field) - ) - assert "$.credentialSubject.Patient.address.city" in return_field.paths - assert "$.credentialSubject.Patient.address[0].city" not in return_field.paths - original_field = DIFField.deserialize( + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"]["Patient"]["address"] = [ { - "path": [ - "$.credentialSubject.Patient.birthDate", - "$.credentialSubject.Patient.address[0].city", - ], - "purpose": "Test", - } - ) - return_field = dif_pres_exch_handler.get_field_updated_path( - deepcopy(original_field) + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + ] + original_path = "$.credentialSubject.Patient[*].address[0].city" + updated_path = await dif_pres_exch_handler.get_updated_path( + cred_dict, original_path + ) + assert updated_path == "$.credentialSubject.Patient[*].address[0].city" + cred_dict["credentialSubject"]["Patient"]["address"] = { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + } + original_path = "$.credentialSubject.Patient[*].address[0].city" + updated_path = await dif_pres_exch_handler.get_updated_path( + cred_dict, original_path ) - assert "$.credentialSubject.Patient.address[0].city" not in return_field.paths - assert "$.credentialSubject.Patient.address.city" in return_field.paths - assert "$.credentialSubject.Patient.birthDate" in return_field.paths + assert updated_path == "$.credentialSubject.Patient[*].address.city" def test_get_dict_keys_from_path(self, profile): dif_pres_exch_handler = DIFPresExchHandler( @@ -3454,3 +3507,47 @@ def test_get_dict_keys_from_path(self, profile): "issuer": "did:key:zUC7DVPRfshooBqmnT2LrMxabCUkRhyyUCu8xKvYRot5aeTLTpPxzZoMyFkMLgKHMPUzdEnJM1EqbxfQd466ed3QuEtUJr8iqKRVfJ4txBa3PRoASaup6fjVAkU9VdbDbs5et64", } assert dif_pres_exch_handler.get_dict_keys_from_path(cred_dict, "issuer") == [] + + cred_dict = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/bbs/v1", + ], + "id": "urn:bnid:_:c14n4", + "type": ["MedicalPass", "VerifiableCredential"], + "credentialSubject": { + "id": "urn:bnid:_:c14n6", + "Patient": { + "@id": "urn:bnid:_:c14n7", + "@type": "fhir:resource-types#Patient", + "address": [ + {"@id": "urn:bnid:_:c14n1", "city": "Рума"}, + {"@id": "urn:bnid:_:c14n1", "city": "Рума"}, + ], + }, + }, + "issuanceDate": "2021-10-01T20:16:40+02:00", + "issuer": "did:key:test", + } + assert dif_pres_exch_handler.get_dict_keys_from_path( + cred_dict, "credentialSubject.Patient.address" + ) == ["@id"] + + @pytest.mark.asyncio + async def test_filter_by_field_keyerror(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"]["Patient"] = { + "@id": "urn:bnid:_:c14n7", + "@type": "fhir:resource-types#Patient", + "address": {"@id": "urn:bnid:_:c14n1", "city": "Рума"}, + } + vc_record_cred = dif_pres_exch_handler.create_vcrecord(cred_dict) + field = DIFField.deserialize( + { + "path": ["$.credentialSubject.Patient[0].address[0].city"], + } + ) + assert not await dif_pres_exch_handler.filter_by_field(field, vc_record_cred) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py index 3ca7bc6a4f..c0e0671390 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py @@ -18,6 +18,7 @@ from .......wallet.base import BaseWallet from .....dif.pres_exch_handler import DIFPresExchHandler, DIFPresExchError +from .....dif.tests.test_data import TEST_CRED_DICT from ....message_types import ( ATTACHMENT_FORMAT, @@ -1334,6 +1335,162 @@ async def test_verify_received_pres_c(self): ) await self.handler.receive_pres(message=dif_pres, pres_ex_record=record) + async def test_verify_received_limit_disclosure_a(self): + dif_proof = deepcopy(DIF_PRES) + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"]["Patient"] = [ + { + "address": [ + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + ] + }, + { + "address": [ + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + ] + }, + ] + dif_proof["verifiableCredential"] = [] + dif_proof["verifiableCredential"].append(cred_dict) + dif_proof["verifiableCredential"].append(cred_dict) + dif_pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_json( + mapping=dif_proof, + ident="dif", + ) + ], + ) + pres_request = deepcopy(DIF_PRES_REQUEST_B) + pres_request["presentation_definition"]["input_descriptors"][0][ + "constraints" + ] = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient[0].address[*].city"], + "purpose": "Test", + } + ], + } + dif_pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_json(pres_request, ident="dif") + ], + ) + record = V20PresExRecord( + pres_ex_id="pxid", + thread_id="thid", + connection_id="conn_id", + initiator="init", + role="role", + state="state", + pres_request=dif_pres_request, + pres=dif_pres, + verified="false", + auto_present=True, + error_msg="error", + ) + await self.handler.receive_pres(message=dif_pres, pres_ex_record=record) + + async def test_verify_received_limit_disclosure_b(self): + dif_proof = deepcopy(DIF_PRES) + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"]["Patient"]["address"] = [ + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + ] + dif_proof["verifiableCredential"] = [] + dif_proof["verifiableCredential"].append(cred_dict) + dif_proof["verifiableCredential"].append(cred_dict) + dif_pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_json( + mapping=dif_proof, + ident="dif", + ) + ], + ) + pres_request = deepcopy(DIF_PRES_REQUEST_B) + pres_request["presentation_definition"]["input_descriptors"][0][ + "constraints" + ] = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient[*].address"], + "purpose": "Test", + } + ], + } + dif_pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_json(pres_request, ident="dif") + ], + ) + record = V20PresExRecord( + pres_ex_id="pxid", + thread_id="thid", + connection_id="conn_id", + initiator="init", + role="role", + state="state", + pres_request=dif_pres_request, + pres=dif_pres, + verified="false", + auto_present=True, + error_msg="error", + ) + await self.handler.receive_pres(message=dif_pres, pres_ex_record=record) + async def test_verify_received_pres_invalid_jsonpath(self): dif_proof = deepcopy(DIF_PRES) dif_proof["presentation_submission"]["descriptor_map"][0][ From 2f60c817cde4c9243d2b9cc01f229ebe76409f8b Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Wed, 6 Oct 2021 23:23:14 -0700 Subject: [PATCH 09/29] Retrigger checks Signed-off-by: Shaanjot Gill From 808aa5ed83480f1cd33cd767980a91ff5454135e Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Fri, 8 Oct 2021 11:06:25 -0700 Subject: [PATCH 10/29] fixes Signed-off-by: Shaanjot Gill --- .../present_proof/dif/pres_exch_handler.py | 74 +++++++++++++++---- .../dif/tests/test_pres_exch_handler.py | 17 +++++ .../protocols/present_proof/v2_0/routes.py | 3 +- 3 files changed, 77 insertions(+), 17 deletions(-) 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 0771cf2861..99947786d5 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -58,6 +58,8 @@ "https://identity.foundation/presentation-exchange/submission/v1" ) PRESENTATION_SUBMISSION_JSONLD_TYPE = "PresentationSubmission" +PYTZ_TIMEZONE_PATTERN = re.compile(r"(([a-zA-Z]+)(?:\/)([a-zA-Z]+))") +LIST_INDEX_PATTERN = re.compile(r"\[(\W+)\]|\[(\d+)\]") class DIFPresExchError(BaseError): @@ -517,15 +519,14 @@ def reveal_doc(self, credential_dict: dict, constraints: Constraints): unflatten_dict = {} for field in constraints._fields: for path in field.paths: - if "[*]" in path: - path = path.replace("[*]", "[0]") jsonpath = parse(path) match = jsonpath.find(credential_dict) if len(match) == 0: continue for match_item in match: full_path = str(match_item.full_path) - if bool(re.search(pattern=r"\[[0-9]+\]", string=full_path)): + if bool(LIST_INDEX_PATTERN.search(full_path)): + full_path = re.sub(r"\[(\W+)\]|\[(\d+)\]", "[0]", full_path) full_path = full_path.replace(".[", "[") unflatten_dict[full_path] = {} explicit_key_path = None @@ -599,6 +600,16 @@ async def filter_by_field(self, field: DIFField, credential: VCRecord) -> bool: return True return False + def string_to_timezone_aware_datetime( + self, datetime_str: str + ) -> Optional[datetime]: + """Convert string with PYTZ timezone to datetime for comparison.""" + if PYTZ_TIMEZONE_PATTERN.search(datetime_str): + result = PYTZ_TIMEZONE_PATTERN.search(datetime_str).group(1) + datetime_str = datetime_str.replace(result, "") + return dateutil_parser(datetime_str).replace(tzinfo=pytz.timezone(result)) + return None + def validate_patch(self, to_check: any, _filter: Filter) -> bool: """ Apply filter on match_value. @@ -623,7 +634,14 @@ def validate_patch(self, to_check: any, _filter: Filter) -> bool: if isinstance(to_check, str): if _filter.fmt == "date" or _filter.fmt == "date-time": try: - to_compare_date = dateutil_parser(to_check) + to_compare_date = ( + self.string_to_timezone_aware_datetime(to_check) + ) + if not to_compare_date: + utc = pytz.UTC + to_compare_date = dateutil_parser(to_check).replace( + tzinfo=utc + ) if isinstance(to_compare_date, datetime): return True except (ParserError, TypeError): @@ -749,10 +767,16 @@ def exclusive_minimum_check(self, val: any, _filter: Filter) -> bool: if _filter.fmt: utc = pytz.UTC if _filter.fmt == "date" or _filter.fmt == "date-time": - to_compare_date = dateutil_parser(_filter.exclusive_min).replace( - tzinfo=utc + to_compare_date = self.string_to_timezone_aware_datetime( + _filter.exclusive_min ) - given_date = dateutil_parser(str(val)).replace(tzinfo=utc) + if not to_compare_date: + to_compare_date = dateutil_parser( + _filter.exclusive_min + ).replace(tzinfo=utc) + given_date = self.string_to_timezone_aware_datetime(str(val)) + if not given_date: + given_date = dateutil_parser(str(val)).replace(tzinfo=utc) return given_date > to_compare_date else: if self.is_numeric(val): @@ -778,10 +802,16 @@ def exclusive_maximum_check(self, val: any, _filter: Filter) -> bool: if _filter.fmt: utc = pytz.UTC if _filter.fmt == "date" or _filter.fmt == "date-time": - to_compare_date = dateutil_parser(_filter.exclusive_max).replace( - tzinfo=utc + to_compare_date = self.string_to_timezone_aware_datetime( + _filter.exclusive_max ) - given_date = dateutil_parser(str(val)).replace(tzinfo=utc) + if not to_compare_date: + to_compare_date = dateutil_parser( + _filter.exclusive_max + ).replace(tzinfo=utc) + given_date = self.string_to_timezone_aware_datetime(str(val)) + if not given_date: + given_date = dateutil_parser(str(val)).replace(tzinfo=utc) return given_date < to_compare_date else: if self.is_numeric(val): @@ -807,10 +837,16 @@ def maximum_check(self, val: any, _filter: Filter) -> bool: if _filter.fmt: utc = pytz.UTC if _filter.fmt == "date" or _filter.fmt == "date-time": - to_compare_date = dateutil_parser(_filter.maximum).replace( - tzinfo=utc + to_compare_date = self.string_to_timezone_aware_datetime( + _filter.maximum ) - given_date = dateutil_parser(str(val)).replace(tzinfo=utc) + if not to_compare_date: + to_compare_date = dateutil_parser(_filter.maximum).replace( + tzinfo=utc + ) + given_date = self.string_to_timezone_aware_datetime(str(val)) + if not given_date: + given_date = dateutil_parser(str(val)).replace(tzinfo=utc) return given_date <= to_compare_date else: if self.is_numeric(val): @@ -836,10 +872,16 @@ def minimum_check(self, val: any, _filter: Filter) -> bool: if _filter.fmt: utc = pytz.UTC if _filter.fmt == "date" or _filter.fmt == "date-time": - to_compare_date = dateutil_parser(_filter.minimum).replace( - tzinfo=utc + to_compare_date = self.string_to_timezone_aware_datetime( + _filter.minimum ) - given_date = dateutil_parser(str(val)).replace(tzinfo=utc) + if not to_compare_date: + to_compare_date = dateutil_parser(_filter.minimum).replace( + tzinfo=utc + ) + given_date = self.string_to_timezone_aware_datetime(str(val)) + if not given_date: + given_date = dateutil_parser(str(val)).replace(tzinfo=utc) return given_date >= to_compare_date else: if self.is_numeric(val): 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 de7fa5cdc9..796f2fb7fd 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 @@ -1,4 +1,5 @@ import asyncio +from datetime import datetime import pytest from asynctest import mock as async_mock @@ -3551,3 +3552,19 @@ async def test_filter_by_field_keyerror(self, profile): } ) assert not await dif_pres_exch_handler.filter_by_field(field, vc_record_cred) + + def test_string_to_timezone_aware_datetime(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + test_datetime_str = "2021-09-28T16:09:00EUROPE/BELGRADE" + assert isinstance( + dif_pres_exch_handler.string_to_timezone_aware_datetime(test_datetime_str), + datetime, + ) + assert ( + dif_pres_exch_handler.string_to_timezone_aware_datetime( + "2020-09-28T11:00:00+00:00" + ) + is None + ) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/routes.py b/aries_cloudagent/protocols/present_proof/v2_0/routes.py index afcf317ab7..264e5d4f16 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/routes.py @@ -496,6 +496,8 @@ async def present_proof_credentials_list(request: web.BaseRequest): "presentation_definition" ).get("input_descriptors") claim_fmt = dif_pres_request.get("presentation_definition").get("format") + if claim_fmt and len(claim_fmt.keys()) > 0: + claim_fmt = ClaimFormat.deserialize(claim_fmt) input_descriptors = [] for input_desc_dict in input_descriptors_list: input_descriptors.append(InputDescriptors.deserialize(input_desc_dict)) @@ -519,7 +521,6 @@ async def present_proof_credentials_list(request: web.BaseRequest): if limit_disclosure: proof_type = [BbsBlsSignature2020.signature_type] if claim_fmt: - claim_fmt = ClaimFormat.deserialize(claim_fmt) if claim_fmt.ldp_vp: if "proof_type" in claim_fmt.ldp_vp: proof_types = claim_fmt.ldp_vp.get("proof_type") From 09e5d6b6f9e0a79f5f16bcd5dfb0949cb43be534 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Sat, 9 Oct 2021 02:09:33 -0700 Subject: [PATCH 11/29] support multi level nested req Signed-off-by: Shaanjot Gill --- .../protocols/present_proof/dif/pres_exch.py | 2 +- .../present_proof/dif/tests/test_pres_exch.py | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py index a00942c35b..b3e27b2a42 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py @@ -146,7 +146,7 @@ class Meta: _from = fields.Str(description="From", required=False, data_key="from") # Self References from_nested = fields.List( - fields.Nested(lambda: SubmissionRequirementsSchema(exclude=("from_nested",))), + fields.Nested(lambda: SubmissionRequirementsSchema()), required=False, ) 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 417b11edaa..20625bd689 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 @@ -105,6 +105,53 @@ def test_submission_requirements_from_nested(self): ).serialize() assert expected_result == actual_result + def test_submission_requirements_from_nested_of_nested(self): + nested_submission_req_json = """ + { + "name": "Лабораторијски резултати", + "purpose": "Morbilli virus критеријум", + "rule": "pick", + "count": 1, + "from_nested": [ + { + "name": "Лични подаци", + "purpose": "Морамо идентификовати субјекта акредитива", + "rule": "pick", + "count": 1, + "from": "Patient" + }, + { + "name": "Лабораторијски резултати", + "purpose": "Morbilli virus критеријум", + "rule": "pick", + "count": 1, + "from_nested": [ + { + "name": "Негативан тест у последња 24 сата", + "purpose": "Незаразност", + "rule": "pick", + "count": 1, + "from": "PCR" + }, + { + "name": "Тест атнитела", + "purpose": "Имуност", + "rule": "pick", + "count": 1, + "from": "IgG" + } + ] + } + ] + } + """ + expected_result = json.loads(nested_submission_req_json) + assert SubmissionRequirements.deserialize(nested_submission_req_json) + actual_result = ( + SubmissionRequirements.deserialize(nested_submission_req_json) + ).serialize() + assert expected_result == actual_result + def test_submission_requirements_from_missing(self): test_json = """ { From 045f03dc928c43141324454732ed3320ef4c0cd3 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Mon, 11 Oct 2021 20:16:27 -0700 Subject: [PATCH 12/29] independent evaluation of submission requirement Signed-off-by: Shaanjot Gill --- .../protocols/present_proof/dif/pres_exch_handler.py | 1 - .../protocols/present_proof/dif/tests/test_data.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 99947786d5..49675a5ac6 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -298,7 +298,6 @@ async def make_requirement( ) return requirement requirement = Requirement( - count=len(srs), nested_req=[], ) for submission_requirement in srs: 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 2d9123beb5..4508a5bb4d 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py @@ -2333,8 +2333,8 @@ def get_test_data(): ) ) pd_json_list = [ - (pres_exch_multiple_srs_not_met, 0), - (pres_exch_multiple_srs_met, 4), + (pres_exch_multiple_srs_not_met, 6), + (pres_exch_multiple_srs_met, 6), (pres_exch_datetime_minimum_met, 6), (pres_exch_datetime_maximum_met, 6), (pres_exch_nested_srs_a, 4), From 5e765611cad2650acb2f03df42b05ea45f7265e1 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Mon, 11 Oct 2021 20:39:54 -0700 Subject: [PATCH 13/29] Retrigger checks Signed-off-by: Shaanjot Gill From 0558c8c19528f2a22285ac5b1eca7c4bc5b711d1 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Thu, 28 Oct 2021 14:12:05 -0700 Subject: [PATCH 14/29] Auto connect from author to endoprser on startup Signed-off-by: Ian Costanzo --- aries_cloudagent/config/argparse.py | 23 +++++- .../endorse_transaction/v1_0/routes.py | 70 ++++++++++++++++++- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index 9aee6604f7..5e12ddf275 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -1535,6 +1535,17 @@ def add_arguments(self, parser: ArgumentParser): "directly." ), ) + parser.add_argument( + "--endorser-invitation", + type=str, + metavar="", + env_var="ACAPY_ENDORSER_INVITATION", + help=( + "For transaction Authors, specify the the invitation used to " + "connect to the Endorser agent who will be endorsing transactions. " + "Note this is a multi-use invitation created by the Endorser agent." + ), + ) parser.add_argument( "--endorser-public-did", type=str, @@ -1542,8 +1553,7 @@ def add_arguments(self, parser: ArgumentParser): env_var="ACAPY_ENDORSER_PUBLIC_DID", help=( "For transaction Authors, specify the the public DID of the Endorser " - "agent who will be endorsing transactions. Note this requires that " - "the connection be made using the Endorser's public DID." + "agent who will be endorsing transactions." ), ) parser.add_argument( @@ -1605,6 +1615,15 @@ def get_settings(self, args: Namespace): elif args.endorser_protocol_role == ENDORSER_ENDORSER: settings["endorser.endorser"] = True + if args.endorser_invitation: + if settings["endorser.author"]: + settings["endorser.endorser_invitation"] = args.endorser_invitation + else: + raise ArgsParseError( + "Parameter --endorser-public-did should only be set for transaction " + "Authors" + ) + if args.endorser_public_did: if settings["endorser.author"]: settings["endorser.endorser_public_did"] = args.endorser_public_did diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py index 960c168818..d525dc078f 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py @@ -1,6 +1,7 @@ """Endorse Transaction handling admin routes.""" import json +import logging from aiohttp import web from aiohttp_apispec import ( @@ -22,11 +23,18 @@ from ....messaging.models.base import BaseModelError from ....messaging.models.openapi import OpenAPISchema from ....messaging.valid import UUIDFour +from ....protocols.connections.v1_0.manager import ConnectionManager +from ....protocols.connections.v1_0.messages.connection_invitation import ( + ConnectionInvitation, +) from ....storage.error import StorageError, StorageNotFoundError from .manager import TransactionManager, TransactionManagerError from .models.transaction_record import TransactionRecord, TransactionRecordSchema from .transaction_jobs import TransactionJob +from .util import is_author_role, get_endorser_connection_id + +LOGGER = logging.getLogger(__name__) class TransactionListSchema(OpenAPISchema): @@ -705,13 +713,69 @@ def register_events(event_bus: EventBus): async def on_startup_event(profile: Profile, event: Event): """Handle any events we need to support.""" - print(">>> TODO Received STARTUP event") - pass + # auto setup is only for authors + if not is_author_role(profile): + return + + # see if we have an invitation to connect to the endorser + endorser_invitation = profile.settings.get_value("endorser.endorser_invitation") + if not endorser_invitation: + # no invitation, we can't connect automatically + return + + # see if we need to initiate an endorser connection + endorser_alias = profile.settings.get_value("endorser.endorser_alias") + if not endorser_alias: + # no alias is specified for the endorser connection + return + + connection_id = await get_endorser_connection_id(profile) + if connection_id: + # there is already a connection + return + + endorser_did = profile.settings.get_value("endorser.endorser_did") + if not endorser_did: + # TBD possibly bail at this point, we can't configure the connection + # for now just continue + pass + + try: + # OK, we are an author, we have no endorser connection but we have enough info + # to automatically initiate the connection + conn_mgr = ConnectionManager(profile) + conn_record = await conn_mgr.receive_invitation( + invitation=ConnectionInvitation.from_url(endorser_invitation), + auto_accept=True, + ) + + # configure the connection role and info + transaction_mgr = TransactionManager(profile) + await transaction_mgr.set_transaction_my_job( + record=conn_record, + transaction_my_job=TransactionJob.TRANSACTION_AUTHOR.name, + ) + + async with profile.session() as session: + value = await conn_record.metadata_get(session, "endorser_info") + if value: + value["endorser_did"] = endorser_did + value["endorser_name"] = endorser_alias + else: + value = {"endorser_did": endorser_did, "endorser_name": endorser_alias} + await conn_record.metadata_set(session, key="endorser_info", value=value) + + except Exception as e: + # log the error, but continue + LOGGER.exception( + "Error accepting endorser invitation/configuring endorser connection: %s", + str(e), + ) async def on_shutdown_event(profile: Profile, event: Event): """Handle any events we need to support.""" - print(">>> TODO Received SHUTDOWN event") + # nothing to do for now ... pass From adb6d73e60a2ce69f9ca716fc19ae95c2605f9f0 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 2 Nov 2021 14:09:35 -0700 Subject: [PATCH 15/29] Incorporate endorser auto-connect into faber demo Signed-off-by: Ian Costanzo --- .../endorse_transaction/v1_0/routes.py | 7 ++- demo/runners/agent_container.py | 30 +++++++---- demo/runners/support/agent.py | 52 ++++++++++++++++--- 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py index d525dc078f..708d7c9427 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py @@ -1,5 +1,6 @@ """Endorse Transaction handling admin routes.""" +import asyncio import json import logging @@ -713,6 +714,7 @@ def register_events(event_bus: EventBus): async def on_startup_event(profile: Profile, event: Event): """Handle any events we need to support.""" + # auto setup is only for authors if not is_author_role(profile): return @@ -734,7 +736,7 @@ async def on_startup_event(profile: Profile, event: Event): # there is already a connection return - endorser_did = profile.settings.get_value("endorser.endorser_did") + endorser_did = profile.settings.get_value("endorser.endorser_public_did") if not endorser_did: # TBD possibly bail at this point, we can't configure the connection # for now just continue @@ -747,9 +749,10 @@ async def on_startup_event(profile: Profile, event: Event): conn_record = await conn_mgr.receive_invitation( invitation=ConnectionInvitation.from_url(endorser_invitation), auto_accept=True, + alias=endorser_alias, ) - # configure the connection role and info + # configure the connection role and info (don't need to wait for the connection) transaction_mgr = TransactionManager(profile) await transaction_mgr.set_transaction_my_job( record=conn_record, diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index a6ac28aa36..01ecdbc177 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -654,6 +654,25 @@ async def initialize( await self.agent.register_did(cred_type=CRED_FORMAT_INDY) log_msg("Created public DID") + # if we are endorsing, create the endorser agent first, then we can use the + # multi-use invitation to auto-connect the agent on startup + if create_endorser_agent: + self.endorser_agent = await start_endorser_agent( + self.start_port + 7, self.genesis_txns + ) + if not self.endorser_agent: + raise Exception("Endorser agent returns None :-(") + # if not await connect_wallet_to_endorser(self.agent, self.endorser_agent): + # raise Exception("Endorser setup FAILED :-(") + + # set the endorser invite so the agent can auto-connect + self.agent.endorser_invite = ( + self.endorser_agent.endorser_multi_invitation_url + ) + self.agent.endorser_did = self.endorser_agent.endorser_public_did + else: + self.endorser_agent = None + with log_timer("Startup duration:"): await self.agent.start_process() @@ -683,17 +702,6 @@ async def initialize( if not await connect_wallet_to_mediator(self.agent, self.mediator_agent): raise Exception("Mediation setup FAILED :-(") - if create_endorser_agent: - self.endorser_agent = await start_endorser_agent( - self.start_port + 7, self.genesis_txns - ) - if not self.endorser_agent: - raise Exception("Endorser agent returns None :-(") - if not await connect_wallet_to_endorser(self.agent, self.endorser_agent): - raise Exception("Endorser setup FAILED :-(") - else: - self.endorser_agent = None - if self.public_did and self.cred_type == CRED_FORMAT_JSON_LD: # create did of appropriate type data = {"method": DID_METHOD_KEY, "options": {"key_type": KEY_TYPE_BLS}} diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 9b63a706f5..69bcc9fc43 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -150,6 +150,7 @@ def __init__( self.postgres = DEFAULT_POSTGRES if postgres is None else postgres self.tails_server_base_url = tails_server_base_url self.endorser_role = endorser_role + self.endorser_invite = None # set this later self.extra_args = extra_args self.trace_enabled = TRACE_ENABLED self.trace_target = TRACE_TARGET @@ -396,8 +397,16 @@ def get_agent_args(self): ("--auto-write-transactions",), ("--auto-create-revocation-transactions",), ("--endorser-alias", "endorser"), + ("--endorser-public-did", self.endorser_did), ] ) + if self.endorser_invite: + result.extend( + ( + "--endorser-invitation", + self.endorser_invite, + ) + ) elif self.endorser_role == "endorser": result.extend( [ @@ -553,6 +562,9 @@ async def register_or_switch_wallet( log_msg("Mediation setup FAILED :-(") raise Exception("Mediation setup FAILED :-(") + # if endorser, endorse the wallet ledger operations + # TODO + self.log(f"Created NEW wallet {target_wallet_name}") return True @@ -1262,11 +1274,26 @@ def connection_ready(self): return self._connection_ready.done() and self._connection_ready.result() async def handle_connections(self, message): + # author responds to a multi-use invitation + if message["state"] == "request": + self.endorser_connection_id = message["connection_id"] + self._connection_ready = asyncio.Future() + + # finish off the connection if message["connection_id"] == self.endorser_connection_id: if message["state"] == "active" and not self._connection_ready.done(): self.log("Endorser Connected") self._connection_ready.set_result(True) + # setup endorser meta-data on our connection + log_msg("Setup endorser agent meta-data ...") + await self.admin_POST( + "/transactions/" + + self.endorser_connection_id + + "/set-endorser-role", + params={"transaction_my_job": "TRANSACTION_ENDORSER"}, + ) + async def handle_basicmessages(self, message): self.log("Received message:", message["content"]) @@ -1286,6 +1313,22 @@ async def start_endorser_agent(start_port, genesis): log_msg("Endorser Endpoint URL is at:", endorser_agent.endpoint) log_msg("Endorser webhooks listening on:", start_port + 2) + # get a reusable invitation to connect to this endorser + log_msg("Generate endorser multi-use invite ...") + endorser_agent.endorser_connection_id = None + endorser_agent.endorser_public_did = None + endorser_connection = await endorser_agent.admin_POST( + "/connections/create-invitation?alias=EndorserMultiuse&auto_accept=true&multi_use=true" + ) + endorser_agent.endorser_multi_connection = endorser_connection + endorser_agent.endorser_connection_id = endorser_connection["connection_id"] + endorser_agent.endorser_multi_invitation = endorser_connection["invitation"] + endorser_agent.endorser_multi_invitation_url = endorser_connection["invitation_url"] + + endorser_agent_public_did = await endorser_agent.admin_GET("/wallet/did/public") + endorser_did = endorser_agent_public_did["result"]["did"] + endorser_agent.endorser_public_did = endorser_did + return endorser_agent @@ -1312,19 +1355,12 @@ async def connect_wallet_to_endorser(agent, endorser_agent): log_msg("Connected agent to endorser:", agent.ident, endorser_agent.ident) # setup endorser meta-data on our connection - log_msg("Setup endorser agent meta-data ...") - await endorser_agent.admin_POST( - "/transactions/" + endorser_agent.endorser_connection_id + "/set-endorser-role", - params={"transaction_my_job": "TRANSACTION_ENDORSER"}, - ) - await asyncio.sleep(1.0) log_msg("Setup author agent meta-data ...") await agent.admin_POST( f"/transactions/{agent.endorser_connection_id }/set-endorser-role", params={"transaction_my_job": "TRANSACTION_AUTHOR"}, ) - endorser_agent_public_did = await endorser_agent.admin_GET("/wallet/did/public") - endorser_did = endorser_agent_public_did["result"]["did"] + endorser_did = endorser_agent.endorser_public_did await agent.admin_POST( f"/transactions/{agent.endorser_connection_id}/set-endorser-info", params={"endorser_did": endorser_did, "endorser_name": "endorser"}, From 7000f8e08137c6df6dd5eec59a828daeccca7384 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 2 Nov 2021 14:18:50 -0700 Subject: [PATCH 16/29] Fix imports Signed-off-by: Ian Costanzo --- aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py index 708d7c9427..a61afd2deb 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py @@ -1,6 +1,5 @@ """Endorse Transaction handling admin routes.""" -import asyncio import json import logging From 22876a93779af20f45093f4bdf60ba8d82e16e99 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 2 Nov 2021 15:44:24 -0700 Subject: [PATCH 17/29] Fix integration tests Signed-off-by: Ian Costanzo --- demo/runners/agent_container.py | 3 +-- demo/runners/faber.py | 2 ++ demo/runners/support/agent.py | 13 +++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index 01ecdbc177..d51e13cc3b 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -662,8 +662,6 @@ async def initialize( ) if not self.endorser_agent: raise Exception("Endorser agent returns None :-(") - # if not await connect_wallet_to_endorser(self.agent, self.endorser_agent): - # raise Exception("Endorser setup FAILED :-(") # set the endorser invite so the agent can auto-connect self.agent.endorser_invite = ( @@ -696,6 +694,7 @@ async def initialize( public_did=self.public_did, webhook_port=None, mediator_agent=self.mediator_agent, + endorser_agent=self.endorser_agent, ) elif self.mediation: # we need to pre-connect the agent to its mediator diff --git a/demo/runners/faber.py b/demo/runners/faber.py index 8fb4022d43..ec0e60d886 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -468,12 +468,14 @@ async def main(args): webhook_port=faber_agent.agent.get_new_webhook_port(), public_did=True, mediator_agent=faber_agent.mediator_agent, + endorser_agent=faber_agent.endorser_agent, ) else: created = await faber_agent.agent.register_or_switch_wallet( target_wallet_name, public_did=True, mediator_agent=faber_agent.mediator_agent, + endorser_agent=faber_agent.endorser_agent, cred_type=faber_agent.cred_type, ) # create a schema and cred def for the new wallet diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 69bcc9fc43..c7ab1a667d 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -150,6 +150,7 @@ def __init__( self.postgres = DEFAULT_POSTGRES if postgres is None else postgres self.tails_server_base_url = tails_server_base_url self.endorser_role = endorser_role + self.endorser_did = None # set this later self.endorser_invite = None # set this later self.extra_args = extra_args self.trace_enabled = TRACE_ENABLED @@ -397,9 +398,14 @@ def get_agent_args(self): ("--auto-write-transactions",), ("--auto-create-revocation-transactions",), ("--endorser-alias", "endorser"), - ("--endorser-public-did", self.endorser_did), ] ) + if self.endorser_did: + result.extend( + [ + ("--endorser-public-did", self.endorser_did), + ] + ) if self.endorser_invite: result.extend( ( @@ -483,6 +489,7 @@ async def register_or_switch_wallet( webhook_port: int = None, mediator_agent=None, cred_type: str = CRED_FORMAT_INDY, + endorser_agent=None, ): if webhook_port is not None: await self.listen_webhooks(webhook_port) @@ -563,7 +570,9 @@ async def register_or_switch_wallet( raise Exception("Mediation setup FAILED :-(") # if endorser, endorse the wallet ledger operations - # TODO + if endorser_agent: + if not await connect_wallet_to_endorser(self, endorser_agent): + raise Exception("Endorser setup FAILED :-(") self.log(f"Created NEW wallet {target_wallet_name}") return True From adf2f343240c93c411a0b4b4650989d88205d9c4 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 2 Nov 2021 16:49:03 -0700 Subject: [PATCH 18/29] Code formatting Signed-off-by: Ian Costanzo --- demo/runners/support/agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index c7ab1a667d..9b6998dc69 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -150,7 +150,7 @@ def __init__( self.postgres = DEFAULT_POSTGRES if postgres is None else postgres self.tails_server_base_url = tails_server_base_url self.endorser_role = endorser_role - self.endorser_did = None # set this later + self.endorser_did = None # set this later self.endorser_invite = None # set this later self.extra_args = extra_args self.trace_enabled = TRACE_ENABLED @@ -572,7 +572,7 @@ async def register_or_switch_wallet( # if endorser, endorse the wallet ledger operations if endorser_agent: if not await connect_wallet_to_endorser(self, endorser_agent): - raise Exception("Endorser setup FAILED :-(") + raise Exception("Endorser setup FAILED :-(") self.log(f"Created NEW wallet {target_wallet_name}") return True From 88c7d11a1e43f5fc91a9e4886c886719e6177455 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 3 Nov 2021 16:04:22 -0400 Subject: [PATCH 19/29] fix: use named tuple instead of dataclass in mediation invite store Signed-off-by: Daniel Bluhm --- .../coordinate_mediation/mediation_invite_store.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/aries_cloudagent/protocols/coordinate_mediation/mediation_invite_store.py b/aries_cloudagent/protocols/coordinate_mediation/mediation_invite_store.py index d40c58602b..8125bdc94f 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/mediation_invite_store.py +++ b/aries_cloudagent/protocols/coordinate_mediation/mediation_invite_store.py @@ -5,17 +5,15 @@ Enables having the mediation invite config be the same for `provision` and `starting` commands. """ -import dataclasses import json -from typing import Optional +from typing import NamedTuple, Optional from aries_cloudagent.storage.base import BaseStorage from aries_cloudagent.storage.error import StorageNotFoundError from aries_cloudagent.storage.record import StorageRecord -@dataclasses.dataclass -class MediationInviteRecord: +class MediationInviteRecord(NamedTuple): """A record to store mediation invites and their freshness.""" invite: str From 8d52ea1c75692fa506959ecac4e59bc850094c71 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Thu, 4 Nov 2021 04:01:34 -0700 Subject: [PATCH 20/29] issue#1457 updates Signed-off-by: Shaanjot Gill --- .../protocols/present_proof/dif/pres_exch.py | 60 ++++++++++++- .../present_proof/dif/pres_exch_handler.py | 81 ++++++++---------- .../present_proof/dif/tests/test_data.py | 35 ++++++-- .../present_proof/dif/tests/test_pres_exch.py | 58 +++++++++++++ .../dif/tests/test_pres_exch_handler.py | 77 ++++++++++++++++- .../present_proof/v2_0/formats/dif/handler.py | 48 +++++++---- .../v2_0/formats/dif/tests/test_handler.py | 83 ++++++++++++++++++ .../protocols/present_proof/v2_0/routes.py | 43 +++++++--- .../present_proof/v2_0/tests/test_routes.py | 84 +++++++++++++++++++ demo/runners/faber.py | 1 + 10 files changed, 483 insertions(+), 87 deletions(-) diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py index b3e27b2a42..0131ef9143 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py @@ -180,7 +180,7 @@ def __init__( uri: str = None, required: bool = None, ): - """Initialize InputDescriptors.""" + """Initialize SchemaInputDescriptor.""" self.uri = uri self.required = required @@ -201,6 +201,58 @@ class Meta: required = fields.Bool(description="Required", required=False) +class SchemasInputDescriptorFilter(BaseModel): + """SchemasInputDescriptorFilter.""" + + class Meta: + """InputDescriptor Schemas filter metadata.""" + + schema_class = "SchemasInputDescriptorFilterSchema" + + def __init__( + self, + *, + oneOf: bool = False, + uri_groups: Sequence[Sequence[SchemaInputDescriptor]] = None, + ): + """Initialize SchemasInputDescriptorFilter.""" + self.oneOf = oneOf + self.uri_groups = uri_groups + + +class SchemasInputDescriptorFilterSchema(BaseModelSchema): + """Single SchemasInputDescriptorFilterSchema Schema.""" + + class Meta: + """SchemasInputDescriptorFilterSchema metadata.""" + + model_class = SchemasInputDescriptorFilter + unknown = EXCLUDE + + uri_groups = fields.List(fields.List(fields.Nested(SchemaInputDescriptorSchema))) + oneOf = fields.Bool(description="oneOf", required=False) + + @pre_load + def extract_info(self, data, **kwargs): + """deserialize.""" + new_data = {} + if isinstance(data, dict): + if "oneOf" in data: + new_data["oneOf"] = True + uri_group_list_of_list = [] + uri_group_list = data.get("oneOf") + for uri_group in uri_group_list: + if isinstance(uri_group, list): + uri_group_list_of_list.append(uri_group) + else: + uri_group_list_of_list.append([uri_group]) + new_data["uri_groups"] = uri_group_list_of_list + elif isinstance(data, list): + new_data["uri_groups"] = [data] + data = new_data + return data + + class DIFHolder(BaseModel): """Single Holder object for Constraints.""" @@ -548,7 +600,7 @@ def __init__( purpose: str = None, metadata: dict = None, constraint: Constraints = None, - schemas: Sequence[SchemaInputDescriptor] = None, + schemas: SchemasInputDescriptorFilter = None, ): """Initialize InputDescriptors.""" self.id = id @@ -584,8 +636,8 @@ class Meta: constraint = fields.Nested( ConstraintsSchema, required=False, data_key="constraints" ) - schemas = fields.List( - fields.Nested(SchemaInputDescriptorSchema), required=False, data_key="schema" + schemas = fields.Nested( + SchemasInputDescriptorFilterSchema, required=False, data_key="schema" ) 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 49675a5ac6..3062579fb4 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -388,13 +388,15 @@ async def filter_constraints( 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: + credential_subject_ids = credential.subject_ids + 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 - ): + is_holder_same_as_subject = await self.process_constraint_holders( + subject_ids=credential_subject_ids + ) + if not is_holder_same_as_subject: applicable = False break if not applicable: @@ -595,19 +597,38 @@ async def filter_by_field(self, field: DIFField, credential: VCRecord) -> bool: # No filter in constraint if not field._filter: return True - if self.validate_patch(match_item.value, field._filter): + if ( + isinstance(match_item.value, dict) + and "type" in match_item.value + and "@value" in match_item.value + ): + to_filter_type = match_item.value.get("type") + to_filter_value = match_item.value.get("@value") + if "integer" in to_filter_type: + to_filter_value = int(to_filter_value) + elif "dateTime" in to_filter_type or "date" in to_filter_type: + to_filter_value = self.string_to_timezone_aware_datetime( + to_filter_value + ) + elif "boolean" in to_filter_type: + to_filter_value = bool(to_filter_value) + elif "double" in to_filter_type or "decimal" in to_filter_type: + to_filter_value = float(to_filter_value) + else: + to_filter_value = match_item.value + if self.validate_patch(to_filter_value, field._filter): return True return False - def string_to_timezone_aware_datetime( - self, datetime_str: str - ) -> Optional[datetime]: + def string_to_timezone_aware_datetime(self, datetime_str: str) -> datetime: """Convert string with PYTZ timezone to datetime for comparison.""" if PYTZ_TIMEZONE_PATTERN.search(datetime_str): result = PYTZ_TIMEZONE_PATTERN.search(datetime_str).group(1) datetime_str = datetime_str.replace(result, "") return dateutil_parser(datetime_str).replace(tzinfo=pytz.timezone(result)) - return None + else: + utc = pytz.UTC + return dateutil_parser(datetime_str).replace(tzinfo=utc) def validate_patch(self, to_check: any, _filter: Filter) -> bool: """ @@ -636,11 +657,6 @@ def validate_patch(self, to_check: any, _filter: Filter) -> bool: to_compare_date = ( self.string_to_timezone_aware_datetime(to_check) ) - if not to_compare_date: - utc = pytz.UTC - to_compare_date = dateutil_parser(to_check).replace( - tzinfo=utc - ) if isinstance(to_compare_date, datetime): return True except (ParserError, TypeError): @@ -764,18 +780,11 @@ def exclusive_minimum_check(self, val: any, _filter: Filter) -> bool: """ try: if _filter.fmt: - utc = pytz.UTC if _filter.fmt == "date" or _filter.fmt == "date-time": to_compare_date = self.string_to_timezone_aware_datetime( _filter.exclusive_min ) - if not to_compare_date: - to_compare_date = dateutil_parser( - _filter.exclusive_min - ).replace(tzinfo=utc) given_date = self.string_to_timezone_aware_datetime(str(val)) - if not given_date: - given_date = dateutil_parser(str(val)).replace(tzinfo=utc) return given_date > to_compare_date else: if self.is_numeric(val): @@ -799,18 +808,11 @@ def exclusive_maximum_check(self, val: any, _filter: Filter) -> bool: """ try: if _filter.fmt: - utc = pytz.UTC if _filter.fmt == "date" or _filter.fmt == "date-time": to_compare_date = self.string_to_timezone_aware_datetime( _filter.exclusive_max ) - if not to_compare_date: - to_compare_date = dateutil_parser( - _filter.exclusive_max - ).replace(tzinfo=utc) given_date = self.string_to_timezone_aware_datetime(str(val)) - if not given_date: - given_date = dateutil_parser(str(val)).replace(tzinfo=utc) return given_date < to_compare_date else: if self.is_numeric(val): @@ -834,18 +836,11 @@ def maximum_check(self, val: any, _filter: Filter) -> bool: """ try: if _filter.fmt: - utc = pytz.UTC if _filter.fmt == "date" or _filter.fmt == "date-time": to_compare_date = self.string_to_timezone_aware_datetime( _filter.maximum ) - if not to_compare_date: - to_compare_date = dateutil_parser(_filter.maximum).replace( - tzinfo=utc - ) given_date = self.string_to_timezone_aware_datetime(str(val)) - if not given_date: - given_date = dateutil_parser(str(val)).replace(tzinfo=utc) return given_date <= to_compare_date else: if self.is_numeric(val): @@ -869,18 +864,11 @@ def minimum_check(self, val: any, _filter: Filter) -> bool: """ try: if _filter.fmt: - utc = pytz.UTC if _filter.fmt == "date" or _filter.fmt == "date-time": to_compare_date = self.string_to_timezone_aware_datetime( _filter.minimum ) - if not to_compare_date: - to_compare_date = dateutil_parser(_filter.minimum).replace( - tzinfo=utc - ) given_date = self.string_to_timezone_aware_datetime(str(val)) - if not given_date: - given_date = dateutil_parser(str(val)).replace(tzinfo=utc) return given_date >= to_compare_date else: if self.is_numeric(val): @@ -985,7 +973,9 @@ def subject_is_issuer(self, credential: VCRecord) -> bool: return False async def filter_schema( - self, credentials: Sequence[VCRecord], schemas: Sequence[SchemaInputDescriptor] + self, + credentials: Sequence[VCRecord], + schemas: Sequence[SchemaInputDescriptor], ) -> Sequence[VCRecord]: """ Filter by schema. @@ -1079,11 +1069,12 @@ async def apply_requirements( ) filtered_by_schema = await self.filter_schema( credentials=filtered_creds_by_descriptor_id, - schemas=descriptor.schemas, + schemas=descriptor.schemas.uri_groups[0], ) else: filtered_by_schema = await self.filter_schema( - credentials=credentials, schemas=descriptor.schemas + credentials=credentials, + schemas=descriptor.schemas.uri_groups[0], ) # Filter credentials based upon path expressions specified in constraints filtered = await self.filter_constraints( 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 4508a5bb4d..70e29e1cf3 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py @@ -1328,14 +1328,16 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): "group":[ "A" ], - "schema":[ - { - "uri":"https://www.w3.org/2018/credentials#VerifiableCredential" - }, - { - "uri":"https://w3id.org/citizenship#PermanentResidentCard" - } - ], + "schema": { + "oneOf": [ + { + "uri":"https://www.w3.org/Test#Test" + }, + { + "uri":"https://w3id.org/citizenship#PermanentResidentCard" + } + ] + }, "constraints":{ "fields":[ { @@ -1480,6 +1482,23 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): "uri":"https://w3id.org/citizenship#PermanentResidentCard" } ], + "schema": { + "oneOf": [ + [ + { + "uri":"https://www.w3.org/Test#Test" + }, + { + "uri":"https://w3id.org/citizenship#PermanentResidentCard" + } + ], + [ + { + "uri":"https://w3id.org/Test#Test" + } + ] + ] + }, "constraints":{ "fields":[ { 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 20625bd689..ca4155a79f 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 @@ -12,6 +12,7 @@ Filter, Constraints, VerifiablePresentation, + SchemasInputDescriptorFilter, ) @@ -378,3 +379,60 @@ def test_verifiable_presentation_wrapper(self): } vp = VerifiablePresentation.deserialize(test_vp_dict) assert isinstance(vp, VerifiablePresentation) + + def test_schemas_input_desc_filter(self): + test_schema_list = [ + [ + {"uri": "https://www.w3.org/2018/VC"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + [{"uri": "https://www.w3.org/Test#Test"}], + ] + test_schemas_filter = { + "oneOf": test_schema_list, + } + + deser_schema_filter = SchemasInputDescriptorFilter.deserialize( + test_schemas_filter + ) + assert deser_schema_filter.oneOf + assert deser_schema_filter.uri_groups[0][0].uri == test_schema_list[0][0].get( + "uri" + ) + assert deser_schema_filter.uri_groups[0][1].uri == test_schema_list[0][1].get( + "uri" + ) + assert deser_schema_filter.uri_groups[1][0].uri == test_schema_list[1][0].get( + "uri" + ) + assert isinstance(deser_schema_filter, SchemasInputDescriptorFilter) + + test_schema_list = [ + {"uri": "https://www.w3.org/Test#Test"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ] + test_schemas_filter = { + "oneOf": test_schema_list, + } + + deser_schema_filter = SchemasInputDescriptorFilter.deserialize( + test_schemas_filter + ) + assert deser_schema_filter.oneOf + assert deser_schema_filter.uri_groups[0][0].uri == test_schema_list[0].get( + "uri" + ) + assert deser_schema_filter.uri_groups[1][0].uri == test_schema_list[1].get( + "uri" + ) + assert isinstance(deser_schema_filter, SchemasInputDescriptorFilter) + + deser_schema_filter = SchemasInputDescriptorFilter.deserialize(test_schema_list) + assert not deser_schema_filter.oneOf + assert deser_schema_filter.uri_groups[0][0].uri == test_schema_list[0].get( + "uri" + ) + assert deser_schema_filter.uri_groups[0][1].uri == test_schema_list[1].get( + "uri" + ) + assert isinstance(deser_schema_filter, SchemasInputDescriptorFilter) 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 796f2fb7fd..21f6945585 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 @@ -3553,6 +3553,77 @@ async def test_filter_by_field_keyerror(self, profile): ) assert not await dif_pres_exch_handler.filter_by_field(field, vc_record_cred) + @pytest.mark.asyncio + async def test_filter_by_field_xsd_parser(self, profile): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"] = {} + cred_dict["credentialSubject"]["lprNumber"] = { + "type": "xsd:integer", + "@value": "10", + } + vc_record_cred = dif_pres_exch_handler.create_vcrecord(cred_dict) + field = DIFField.deserialize( + { + "path": ["$.credentialSubject.lprNumber"], + "filter": { + "minimum": 5, + "type": "number", + }, + } + ) + assert await dif_pres_exch_handler.filter_by_field(field, vc_record_cred) + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"] = {} + cred_dict["credentialSubject"]["testDate"] = { + "type": "xsd:dateTime", + "@value": "2020-09-28T11:00:00+00:00", + } + vc_record_cred = dif_pres_exch_handler.create_vcrecord(cred_dict) + field = DIFField.deserialize( + { + "path": ["$.credentialSubject.testDate"], + "filter": {"type": "string", "format": "date", "minimum": "2005-5-16"}, + } + ) + assert await dif_pres_exch_handler.filter_by_field(field, vc_record_cred) + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"] = {} + cred_dict["credentialSubject"]["testFlag"] = { + "type": "xsd:boolean", + "@value": "false", + } + vc_record_cred = dif_pres_exch_handler.create_vcrecord(cred_dict) + field = DIFField.deserialize( + { + "path": ["$.credentialSubject.testFlag"], + } + ) + assert await dif_pres_exch_handler.filter_by_field(field, vc_record_cred) + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"] = {} + cred_dict["credentialSubject"]["testDouble"] = { + "type": "xsd:double", + "@value": "10.2", + } + vc_record_cred = dif_pres_exch_handler.create_vcrecord(cred_dict) + field = DIFField.deserialize( + {"path": ["$.credentialSubject.testDouble"], "filter": {"const": 10.2}} + ) + assert await dif_pres_exch_handler.filter_by_field(field, vc_record_cred) + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"] = {} + cred_dict["credentialSubject"]["test"] = { + "type": ["test"], + "@id": "test", + "test": "val", + } + vc_record_cred = dif_pres_exch_handler.create_vcrecord(cred_dict) + field = DIFField.deserialize({"path": ["$.credentialSubject.test"]}) + assert await dif_pres_exch_handler.filter_by_field(field, vc_record_cred) + def test_string_to_timezone_aware_datetime(self, profile): dif_pres_exch_handler = DIFPresExchHandler( profile, proof_type=BbsBlsSignature2020.signature_type @@ -3562,9 +3633,9 @@ def test_string_to_timezone_aware_datetime(self, profile): dif_pres_exch_handler.string_to_timezone_aware_datetime(test_datetime_str), datetime, ) - assert ( + assert isinstance( dif_pres_exch_handler.string_to_timezone_aware_datetime( "2020-09-28T11:00:00+00:00" - ) - is None + ), + datetime, ) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py index a51bed992d..bd7701c62f 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py @@ -199,16 +199,24 @@ async def create_pres( input_descriptor.constraint.limit_disclosure == "required" ) uri_list = [] - for schema in input_descriptor.schemas: - uri = schema.uri - if schema.required is None: - required = True + one_of_uri_groups = [] + if input_descriptor.schemas: + if input_descriptor.schemas.oneOf: + for schema_uri_group in input_descriptor.schemas.uri_groups: + one_of_uri_groups.append(schema_uri_group) else: - required = schema.required - if required: - uri_list.append(uri) + schema_uris = input_descriptor.schemas.uri_groups[0] + for schema_uri in schema_uris: + if schema_uri.required is None: + required = True + else: + required = schema_uri.required + if required: + uri_list.append(schema_uri.uri) if len(uri_list) == 0: uri_list = None + if len(one_of_uri_groups) == 0: + one_of_uri_groups = None if limit_disclosure: proof_type = [BbsBlsSignature2020.signature_type] dif_handler_proof_type = BbsBlsSignature2020.signature_type @@ -287,13 +295,25 @@ async def create_pres( "BbsBlsSignature2020 and Ed25519Signature2018" " signature types are supported" ) - search = holder.search_credentials( - proof_types=proof_type, pd_uri_list=uri_list - ) - # Defaults to page_size but would like to include all - # For now, setting to 1000 - max_results = 1000 - records = await search.fetch(max_results) + if one_of_uri_groups: + for uri_group in one_of_uri_groups: + search = holder.search_credentials( + proof_types=proof_type, pd_uri_list=uri_group + ) + max_results = 1000 + records = await search.fetch(max_results) + if records and len(records) > 0: + input_descriptor.schemas.uri_groups = [uri_group] + pres_definition.input_descriptors = input_descriptor + break + else: + search = holder.search_credentials( + proof_types=proof_type, pd_uri_list=uri_list + ) + # Defaults to page_size but would like to include all + # For now, setting to 1000 + max_results = 1000 + records = await search.fetch(max_results) # Avoiding addition of duplicate records ( vcrecord_list, diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py index c0e0671390..89466138bc 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py @@ -619,6 +619,89 @@ async def test_create_pres_prover_proof_spec_with_reveal_doc(self): ) assert output[1].data.json_ == DIF_PRES + async def test_create_pres_one_of_filter(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="did:example:489398593", + subject_ids=[ + "did:sov:WgWxqztrNooG92RXvxSTWv", + ], + proof_types=["Ed25519Signature2018"], + schema_ids=["https://example.org/examples/degree.json"], + cred_value={"...", "..."}, + given_id="http://example.edu/credentials/3732", + cred_tags={"some": "tag"}, + record_id="test1", + ) + ] + pres_request = deepcopy(DIF_PRES_REQUEST_B) + pres_request["presentation_definition"]["input_descriptors"][0]["schema"] = { + "oneOf": [ + [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + [{"uri": "https://www.w3.org/Test#Test"}], + ] + } + dif_pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_json(pres_request, ident="dif") + ], + ) + record = V20PresExRecord( + pres_ex_id="pxid", + thread_id="thid", + connection_id="conn_id", + initiator="init", + role="role", + state="state", + pres_request=dif_pres_request, + verified="false", + auto_present=True, + error_msg="error", + ) + request_data = {"dif": {}} + + self.context.injector.bind_instance( + VCHolder, + async_mock.MagicMock( + search_credentials=async_mock.MagicMock( + return_value=async_mock.MagicMock( + fetch=async_mock.CoroutineMock(return_value=cred_list) + ) + ) + ), + ) + + with async_mock.patch.object( + DIFPresExchHandler, + "create_vp", + async_mock.CoroutineMock(), + ) as mock_create_vp: + mock_create_vp.return_value = DIF_PRES + output = await self.handler.create_pres(record, request_data) + assert isinstance(output[0], V20PresFormat) and isinstance( + output[1], AttachDecorator + ) + assert output[1].data.json_ == DIF_PRES + async def test_create_pres_no_challenge(self): dif_pres_req = deepcopy(DIF_PRES_REQUEST_B) del dif_pres_req["options"]["challenge"] diff --git a/aries_cloudagent/protocols/present_proof/v2_0/routes.py b/aries_cloudagent/protocols/present_proof/v2_0/routes.py index 264e5d4f16..ed75aecf64 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/routes.py @@ -504,20 +504,28 @@ async def present_proof_credentials_list(request: web.BaseRequest): record_ids = set() for input_descriptor in input_descriptors: proof_type = None - uri_list = [] limit_disclosure = input_descriptor.constraint.limit_disclosure and ( input_descriptor.constraint.limit_disclosure == "required" ) - for schema in input_descriptor.schemas: - uri = schema.uri - if schema.required is None: - required = True + uri_list = [] + one_of_uri_groups = [] + if input_descriptor.schemas: + if input_descriptor.schemas.oneOf: + for schema_uri_group in input_descriptor.schemas.uri_groups: + one_of_uri_groups.append(schema_uri_group) else: - required = schema.required - if required: - uri_list.append(uri) + schema_uris = input_descriptor.schemas.uri_groups[0] + for schema_uri in schema_uris: + if schema_uri.required is None: + required = True + else: + required = schema_uri.required + if required: + uri_list.append(schema_uri.uri) if len(uri_list) == 0: uri_list = None + if len(one_of_uri_groups) == 0: + one_of_uri_groups = None if limit_disclosure: proof_type = [BbsBlsSignature2020.signature_type] if claim_fmt: @@ -597,11 +605,20 @@ async def present_proof_credentials_list(request: web.BaseRequest): " signature types are supported" ) ) - search = dif_holder.search_credentials( - proof_types=proof_type, - pd_uri_list=uri_list, - ) - records = await search.fetch(count) + if one_of_uri_groups: + for uri_group in one_of_uri_groups: + search = dif_holder.search_credentials( + proof_types=proof_type, pd_uri_list=uri_group + ) + records = await search.fetch(count) + if records and len(records) > 0: + break + else: + search = dif_holder.search_credentials( + proof_types=proof_type, + pd_uri_list=uri_list, + ) + records = await search.fetch(count) # Avoiding addition of duplicate records vcrecord_list, vcrecord_ids_set = await process_vcrecords_return_list( records, record_ids diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py index 6c54e7742f..fbad5077aa 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py @@ -458,6 +458,90 @@ async def test_present_proof_credentials_list_dif(self): ] ) + 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 = [ + async_mock.MagicMock(cred_value={"name": "Credential1"}), + async_mock.MagicMock(cred_value={"name": "Credential2"}), + ] + self.profile.context.injector.bind_instance( + IndyHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + async_mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + async_mock.MagicMock( + search_credentials=async_mock.MagicMock( + return_value=async_mock.MagicMock( + fetch=async_mock.CoroutineMock( + return_value=returned_credentials + ) + ) + ) + ), + ) + pres_request = deepcopy(DIF_PROOF_REQ) + pres_request["presentation_definition"]["input_descriptors"][0]["schema"] = { + "oneOf": [ + [ + {"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 async_mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, async_mock.patch.object( + test_module.web, "json_response", async_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_dif_no_tag_query(self): self.request.match_info = { "pres_ex_id": "123-456-789", diff --git a/demo/runners/faber.py b/demo/runners/faber.py index eaa6145ebd..e4c3b52e23 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -139,6 +139,7 @@ def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracin "@context": [ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/citizenship/v1", + "https://w3id.org/security/bbs/v1", ], "type": [ "VerifiableCredential", From ea70b32b2f38921a09fba9760b887e78f28c0629 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Thu, 4 Nov 2021 04:34:57 -0700 Subject: [PATCH 21/29] reverting independent evaluation change of submission_req Signed-off-by: Shaanjot Gill --- .../present_proof/dif/pres_exch_handler.py | 1 + .../present_proof/dif/tests/test_data.py | 28 ++++++++----------- 2 files changed, 12 insertions(+), 17 deletions(-) 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 3062579fb4..3b07d85018 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -298,6 +298,7 @@ async def make_requirement( ) return requirement requirement = Requirement( + count=len(srs), nested_req=[], ) for submission_requirement in srs: 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 70e29e1cf3..2a256983fc 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py @@ -1330,12 +1330,14 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): ], "schema": { "oneOf": [ - { - "uri":"https://www.w3.org/Test#Test" - }, - { - "uri":"https://w3id.org/citizenship#PermanentResidentCard" - } + [ + { + "uri":"https://www.w3.org/2018/credentials#VerifiableCredential" + }, + { + "uri":"https://w3id.org/citizenship#PermanentResidentCard" + } + ] ] }, "constraints":{ @@ -1474,19 +1476,11 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): "group":[ "A" ], - "schema":[ - { - "uri":"https://www.w3.org/2018/credentials#VerifiableCredential" - }, - { - "uri":"https://w3id.org/citizenship#PermanentResidentCard" - } - ], "schema": { "oneOf": [ [ { - "uri":"https://www.w3.org/Test#Test" + "uri":"https://www.w3.org/2018/credentials#VerifiableCredential" }, { "uri":"https://w3id.org/citizenship#PermanentResidentCard" @@ -2352,8 +2346,8 @@ def get_test_data(): ) ) pd_json_list = [ - (pres_exch_multiple_srs_not_met, 6), - (pres_exch_multiple_srs_met, 6), + (pres_exch_multiple_srs_not_met, 0), + (pres_exch_multiple_srs_met, 4), (pres_exch_datetime_minimum_met, 6), (pres_exch_datetime_maximum_met, 6), (pres_exch_nested_srs_a, 4), From 97db6b01fbeb2ca13c1e5f3fe4ec8eb4db5e9c19 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Thu, 4 Nov 2021 09:52:38 -0700 Subject: [PATCH 22/29] issue_1457 oneOf filtering in pres_exch_handler Signed-off-by: Shaanjot Gill --- .../present_proof/dif/pres_exch_handler.py | 44 ++++++++++++------- .../present_proof/v2_0/formats/dif/handler.py | 16 ++++--- .../protocols/present_proof/v2_0/routes.py | 14 ++++-- .../present_proof/v2_0/tests/test_routes.py | 12 +++-- 4 files changed, 58 insertions(+), 28 deletions(-) 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 3b07d85018..4ccd8023c4 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -49,7 +49,7 @@ Constraints, SubmissionRequirements, Requirement, - SchemaInputDescriptor, + SchemasInputDescriptorFilter, InputDescriptorMapping, PresentationSubmission, ) @@ -976,7 +976,7 @@ def subject_is_issuer(self, credential: VCRecord) -> bool: async def filter_schema( self, credentials: Sequence[VCRecord], - schemas: Sequence[SchemaInputDescriptor], + schemas: SchemasInputDescriptorFilter, ) -> Sequence[VCRecord]: """ Filter by schema. @@ -994,19 +994,31 @@ async def filter_schema( result = [] for credential in credentials: applicable = False - for schema in schemas: - applicable = self.credential_match_schema( - credential=credential, schema_id=schema.uri - ) - if schema.required and not applicable: - break - if applicable: - if schema.uri in [ - EXPANDED_TYPE_CREDENTIALS_CONTEXT_V1_VC_TYPE, - ]: - continue - else: + if schemas.oneOf: + for uri_group in schemas.uri_groups: + if applicable: break + for schema in uri_group: + applicable = self.credential_match_schema( + credential=credential, schema_id=schema.uri + ) + if schema.required and not applicable: + break + else: + uri_group = schemas.uri_groups[0] + for schema in uri_group: + applicable = self.credential_match_schema( + credential=credential, schema_id=schema.uri + ) + if schema.required and not applicable: + break + if applicable: + if schema.uri in [ + EXPANDED_TYPE_CREDENTIALS_CONTEXT_V1_VC_TYPE, + ]: + continue + else: + break if applicable: result.append(credential) return result @@ -1070,12 +1082,12 @@ async def apply_requirements( ) filtered_by_schema = await self.filter_schema( credentials=filtered_creds_by_descriptor_id, - schemas=descriptor.schemas.uri_groups[0], + schemas=descriptor.schemas, ) else: filtered_by_schema = await self.filter_schema( credentials=credentials, - schemas=descriptor.schemas.uri_groups[0], + schemas=descriptor.schemas, ) # Filter credentials based upon path expressions specified in constraints filtered = await self.filter_constraints( diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py index bd7701c62f..2439cb07ee 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py @@ -296,16 +296,22 @@ async def create_pres( " signature types are supported" ) if one_of_uri_groups: + records = [] + cred_group_record_ids = set() for uri_group in one_of_uri_groups: search = holder.search_credentials( proof_types=proof_type, pd_uri_list=uri_group ) max_results = 1000 - records = await search.fetch(max_results) - if records and len(records) > 0: - input_descriptor.schemas.uri_groups = [uri_group] - pres_definition.input_descriptors = input_descriptor - break + cred_group = await search.fetch(max_results) + ( + cred_group_vcrecord_list, + cred_group_vcrecord_ids_set, + ) = await self.process_vcrecords_return_list( + cred_group, cred_group_record_ids + ) + cred_group_record_ids = cred_group_vcrecord_ids_set + records = records + cred_group_vcrecord_list else: search = holder.search_credentials( proof_types=proof_type, pd_uri_list=uri_list diff --git a/aries_cloudagent/protocols/present_proof/v2_0/routes.py b/aries_cloudagent/protocols/present_proof/v2_0/routes.py index c488c5013f..f4c539eb88 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/routes.py @@ -609,13 +609,21 @@ async def present_proof_credentials_list(request: web.BaseRequest): ) ) if one_of_uri_groups: + records = [] + cred_group_record_ids = set() for uri_group in one_of_uri_groups: search = dif_holder.search_credentials( proof_types=proof_type, pd_uri_list=uri_group ) - records = await search.fetch(count) - if records and len(records) > 0: - break + cred_group = await search.fetch(count) + ( + cred_group_vcrecord_list, + cred_group_vcrecord_ids_set, + ) = await process_vcrecords_return_list( + cred_group, cred_group_record_ids + ) + cred_group_record_ids = cred_group_vcrecord_ids_set + records = records + cred_group_vcrecord_list else: search = dif_holder.search_credentials( proof_types=proof_type, diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py index fbad5077aa..84bc940e0a 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py @@ -465,8 +465,12 @@ async def test_present_proof_credentials_list_dif_one_of_filter(self): self.request.query = {"extra_query": {}} returned_credentials = [ - async_mock.MagicMock(cred_value={"name": "Credential1"}), - async_mock.MagicMock(cred_value={"name": "Credential2"}), + async_mock.MagicMock( + cred_value={"name": "Credential1"}, record_id="test_1" + ), + async_mock.MagicMock( + cred_value={"name": "Credential2"}, record_id="test_2" + ), ] self.profile.context.injector.bind_instance( IndyHolder, @@ -537,8 +541,8 @@ async def test_present_proof_credentials_list_dif_one_of_filter(self): 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}, + {"name": "Credential1", "record_id": "test_1"}, + {"name": "Credential2", "record_id": "test_2"}, ] ) From 91eeed4e489c790c005369c929423aa056832f4d Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Thu, 4 Nov 2021 09:53:16 -0700 Subject: [PATCH 23/29] test update Signed-off-by: Shaanjot Gill --- .../dif/tests/test_pres_exch_handler.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) 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 21f6945585..1504f4adf0 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 @@ -34,6 +34,7 @@ Requirement, Filter, SchemaInputDescriptor, + SchemasInputDescriptorFilter, Constraints, DIFField, ) @@ -1562,12 +1563,19 @@ async def test_filter_string(self, setup_tuple, profile): async def test_filter_schema(self, setup_tuple, profile): cred_list, pd_list = setup_tuple dif_pres_exch_handler = DIFPresExchHandler(profile) - tmp_schema_list = [ - SchemaInputDescriptor( - uri="test123", - required=True, - ) - ] + tmp_schema_list = SchemasInputDescriptorFilter( + oneOf=True, + uri_groups=[ + [ + SchemaInputDescriptor( + uri="test123", + required=True, + ), + SchemaInputDescriptor(uri="test321"), + ], + [SchemaInputDescriptor(uri="test789")], + ], + ) assert ( len(await dif_pres_exch_handler.filter_schema(cred_list, tmp_schema_list)) == 0 From f909f1249a2386f851fbdd454ed2db90b789ac68 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Thu, 4 Nov 2021 11:56:27 -0700 Subject: [PATCH 24/29] Support old-style and oob connections for the endorser Signed-off-by: Ian Costanzo --- aries_cloudagent/config/argparse.py | 32 ++++++++++----- .../endorse_transaction/v1_0/routes.py | 35 +++++++++++----- demo/runners/agent_container.py | 7 +++- demo/runners/support/agent.py | 40 ++++++++++++++----- 4 files changed, 82 insertions(+), 32 deletions(-) diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index 5e12ddf275..e653c6ef3e 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -1541,7 +1541,7 @@ def add_arguments(self, parser: ArgumentParser): metavar="", env_var="ACAPY_ENDORSER_INVITATION", help=( - "For transaction Authors, specify the the invitation used to " + "For transaction Authors, specify the invitation used to " "connect to the Endorser agent who will be endorsing transactions. " "Note this is a multi-use invitation created by the Endorser agent." ), @@ -1552,7 +1552,7 @@ def add_arguments(self, parser: ArgumentParser): metavar="", env_var="ACAPY_ENDORSER_PUBLIC_DID", help=( - "For transaction Authors, specify the the public DID of the Endorser " + "For transaction Authors, specify the public DID of the Endorser " "agent who will be endorsing transactions." ), ) @@ -1562,7 +1562,7 @@ def add_arguments(self, parser: ArgumentParser): metavar="", env_var="ACAPY_ENDORSER_ALIAS", help=( - "For transaction Authors, specify the the alias of the Endorser " + "For transaction Authors, specify the alias of the Endorser " "connection that will be used to endorse transactions." ), ) @@ -1615,30 +1615,40 @@ def get_settings(self, args: Namespace): elif args.endorser_protocol_role == ENDORSER_ENDORSER: settings["endorser.endorser"] = True - if args.endorser_invitation: + if args.endorser_public_did: if settings["endorser.author"]: - settings["endorser.endorser_invitation"] = args.endorser_invitation + settings["endorser.endorser_public_did"] = args.endorser_public_did else: raise ArgsParseError( "Parameter --endorser-public-did should only be set for transaction " "Authors" ) - if args.endorser_public_did: + if args.endorser_alias: if settings["endorser.author"]: - settings["endorser.endorser_public_did"] = args.endorser_public_did + settings["endorser.endorser_alias"] = args.endorser_alias else: raise ArgsParseError( - "Parameter --endorser-public-did should only be set for transaction " + "Parameter --endorser-alias should only be set for transaction " "Authors" ) - if args.endorser_alias: + if args.endorser_invitation: if settings["endorser.author"]: - settings["endorser.endorser_alias"] = args.endorser_alias + if not settings.get("endorser.endorser_public_did"): + raise ArgsParseError( + "Parameter --endorser-public-did must be provided if " + "--endorser-invitation is set." + ) + if not settings.get("endorser.endorser_alias"): + raise ArgsParseError( + "Parameter --endorser-alias must be provided if " + "--endorser-invitation is set." + ) + settings["endorser.endorser_invitation"] = args.endorser_invitation else: raise ArgsParseError( - "Parameter --endorser-alias should only be set for transaction " + "Parameter --endorser-invitation should only be set for transaction " "Authors" ) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py index a61afd2deb..da37218d0a 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py @@ -27,6 +27,8 @@ from ....protocols.connections.v1_0.messages.connection_invitation import ( ConnectionInvitation, ) +from ....protocols.out_of_band.v1_0.manager import OutOfBandManager +from ....protocols.out_of_band.v1_0.messages.invitation import InvitationMessage from ....storage.error import StorageError, StorageNotFoundError from .manager import TransactionManager, TransactionManagerError @@ -728,6 +730,7 @@ async def on_startup_event(profile: Profile, event: Event): endorser_alias = profile.settings.get_value("endorser.endorser_alias") if not endorser_alias: # no alias is specified for the endorser connection + # note that alias is required if invitation is specified return connection_id = await get_endorser_connection_id(profile) @@ -737,19 +740,33 @@ async def on_startup_event(profile: Profile, event: Event): endorser_did = profile.settings.get_value("endorser.endorser_public_did") if not endorser_did: - # TBD possibly bail at this point, we can't configure the connection - # for now just continue - pass + # no DID, we can connect but we can't properly setup the connection metadata + # note that DID is required if invitation is specified + return try: # OK, we are an author, we have no endorser connection but we have enough info # to automatically initiate the connection - conn_mgr = ConnectionManager(profile) - conn_record = await conn_mgr.receive_invitation( - invitation=ConnectionInvitation.from_url(endorser_invitation), - auto_accept=True, - alias=endorser_alias, - ) + invite = InvitationMessage.from_url(endorser_invitation) + if invite: + oob_mgr = OutOfBandManager(profile) + conn_record = await oob_mgr.receive_invitation( + invitation=invite, + auto_accept=True, + alias=endorser_alias, + ) + else: + invite = ConnectionInvitation.from_url(endorser_invitation) + if invite: + conn_mgr = ConnectionManager(profile) + conn_record = await conn_mgr.receive_invitation( + invitation=invite, + auto_accept=True, + alias=endorser_alias, + ) + else: + raise Exception("Failed to establish endorser connection, invalid " + "invitation format.") # configure the connection role and info (don't need to wait for the connection) transaction_mgr = TransactionManager(profile) diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index d51e13cc3b..eb027045e4 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -596,6 +596,7 @@ def __init__( self.multitenant = multitenant self.mediation = mediation self.use_did_exchange = use_did_exchange + print("Setting use_did_exchange:", self.use_did_exchange) self.wallet_type = wallet_type self.public_did = public_did self.seed = seed @@ -658,7 +659,9 @@ async def initialize( # multi-use invitation to auto-connect the agent on startup if create_endorser_agent: self.endorser_agent = await start_endorser_agent( - self.start_port + 7, self.genesis_txns + self.start_port + 7, + self.genesis_txns, + use_did_exchange=self.use_did_exchange, ) if not self.endorser_agent: raise Exception("Endorser agent returns None :-(") @@ -1157,7 +1160,7 @@ async def create_agent_with_args(args, ident: str = None): multitenant=args.multitenant, mediation=args.mediation, cred_type=cred_type, - use_did_exchange=args.did_exchange if ("did_exchange" in args) else (aip == 20), + use_did_exchange=(aip == 20) if ("aip" in args) else args.did_exchange, wallet_type=arg_file_dict.get("wallet-type") or args.wallet_type, public_did=public_did, seed="random" if public_did else None, diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 9b6998dc69..3c83a46de3 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -1283,6 +1283,11 @@ def connection_ready(self): return self._connection_ready.done() and self._connection_ready.result() async def handle_connections(self, message): + # inviter: + conn_id = message["connection_id"] + if message["state"] == "invitation": + self.connection_id = conn_id + # author responds to a multi-use invitation if message["state"] == "request": self.endorser_connection_id = message["connection_id"] @@ -1307,7 +1312,7 @@ async def handle_basicmessages(self, message): self.log("Received message:", message["content"]) -async def start_endorser_agent(start_port, genesis): +async def start_endorser_agent(start_port, genesis, use_did_exchange: bool = True): # start mediator agent endorser_agent = EndorserAgent( start_port, @@ -1326,11 +1331,19 @@ async def start_endorser_agent(start_port, genesis): log_msg("Generate endorser multi-use invite ...") endorser_agent.endorser_connection_id = None endorser_agent.endorser_public_did = None - endorser_connection = await endorser_agent.admin_POST( - "/connections/create-invitation?alias=EndorserMultiuse&auto_accept=true&multi_use=true" - ) + endorser_agent.use_did_exchange = use_did_exchange + if use_did_exchange: + endorser_connection = await endorser_agent.admin_POST( + "/out-of-band/create-invitation", + {"handshake_protocols": ["rfc23"]}, + params={"alias": "EndorserMultiuse", "auto_accept": "true", "multi_use": "true"}, + ) + else: + # old-style connection + endorser_connection = await endorser_agent.admin_POST( + "/connections/create-invitation?alias=EndorserMultiuse&auto_accept=true&multi_use=true" + ) endorser_agent.endorser_multi_connection = endorser_connection - endorser_agent.endorser_connection_id = endorser_connection["connection_id"] endorser_agent.endorser_multi_invitation = endorser_connection["invitation"] endorser_agent.endorser_multi_invitation_url = endorser_connection["invitation_url"] @@ -1352,11 +1365,18 @@ async def connect_wallet_to_endorser(agent, endorser_agent): # accept the invitation log_msg("Accept endorser invite ...") - connection = await agent.admin_POST( - "/connections/receive-invitation", - endorser_connection["invitation"], - params={"alias": "endorser"}, - ) + if endorser_agent.use_did_exchange: + connection = await agent.admin_POST( + "/out-of-band/receive-invitation", + endorser_connection["invitation"], + params={"alias": "endorser"}, + ) + else: + connection = await agent.admin_POST( + "/connections/receive-invitation", + endorser_connection["invitation"], + params={"alias": "endorser"}, + ) agent.endorser_connection_id = connection["connection_id"] log_msg("Await endorser connection status ...") From aa3fcc26eebe1477b837900aadd8f7ded942b580 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Thu, 4 Nov 2021 12:28:57 -0700 Subject: [PATCH 25/29] Code formatting Signed-off-by: Ian Costanzo --- .../protocols/endorse_transaction/v1_0/routes.py | 6 ++++-- demo/runners/support/agent.py | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py index da37218d0a..64d361369f 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py @@ -765,8 +765,10 @@ async def on_startup_event(profile: Profile, event: Event): alias=endorser_alias, ) else: - raise Exception("Failed to establish endorser connection, invalid " - "invitation format.") + raise Exception( + "Failed to establish endorser connection, invalid " + "invitation format." + ) # configure the connection role and info (don't need to wait for the connection) transaction_mgr = TransactionManager(profile) diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 3c83a46de3..fd6e401efb 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -1336,7 +1336,11 @@ async def start_endorser_agent(start_port, genesis, use_did_exchange: bool = Tru endorser_connection = await endorser_agent.admin_POST( "/out-of-band/create-invitation", {"handshake_protocols": ["rfc23"]}, - params={"alias": "EndorserMultiuse", "auto_accept": "true", "multi_use": "true"}, + params={ + "alias": "EndorserMultiuse", + "auto_accept": "true", + "multi_use": "true", + }, ) else: # old-style connection From 2b8ea07abc4d36f6237ad76669218a99fe7dd9d8 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Thu, 4 Nov 2021 19:37:20 -0700 Subject: [PATCH 26/29] oneOf filter update Signed-off-by: Shaanjot Gill --- .../protocols/present_proof/dif/pres_exch.py | 3 +- .../present_proof/dif/pres_exch_handler.py | 60 +++++++++++++++---- .../present_proof/v2_0/formats/dif/handler.py | 22 ++++++- .../v2_0/formats/dif/tests/test_handler.py | 14 +++++ .../protocols/present_proof/v2_0/routes.py | 21 ++++++- .../present_proof/v2_0/tests/test_routes.py | 14 +++++ 6 files changed, 115 insertions(+), 19 deletions(-) diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py index 0131ef9143..6e82905280 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py @@ -230,7 +230,7 @@ class Meta: unknown = EXCLUDE uri_groups = fields.List(fields.List(fields.Nested(SchemaInputDescriptorSchema))) - oneOf = fields.Bool(description="oneOf", required=False) + oneOf = fields.Bool(description="oneOf") @pre_load def extract_info(self, data, **kwargs): @@ -248,6 +248,7 @@ def extract_info(self, data, **kwargs): uri_group_list_of_list.append([uri_group]) new_data["uri_groups"] = uri_group_list_of_list elif isinstance(data, list): + new_data["oneOf"] = False new_data["uri_groups"] = [data] data = new_data return data 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 4ccd8023c4..08e88dd671 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -49,6 +49,7 @@ Constraints, SubmissionRequirements, Requirement, + SchemaInputDescriptor, SchemasInputDescriptorFilter, InputDescriptorMapping, PresentationSubmission, @@ -973,6 +974,22 @@ def subject_is_issuer(self, credential: VCRecord) -> bool: return True return False + async def _credential_match_schema_filter_helper( + self, credential: VCRecord, filter: Sequence[Sequence[SchemaInputDescriptor]] + ) -> bool: + """credential_match_schema for SchemasInputDescriptorFilter.uri_groups.""" + applicable = False + for uri_group in filter: + if applicable: + return True + for schema in uri_group: + applicable = self.credential_match_schema( + credential=credential, schema_id=schema.uri + ) + if schema.required and not applicable: + break + return applicable + async def filter_schema( self, credentials: Sequence[VCRecord], @@ -995,15 +1012,9 @@ async def filter_schema( for credential in credentials: applicable = False if schemas.oneOf: - for uri_group in schemas.uri_groups: - if applicable: - break - for schema in uri_group: - applicable = self.credential_match_schema( - credential=credential, schema_id=schema.uri - ) - if schema.required and not applicable: - break + applicable = await self._credential_match_schema_filter_helper( + credential=credential, filter=schemas.uri_groups + ) else: uri_group = schemas.uri_groups[0] for schema in uri_group: @@ -1353,11 +1364,17 @@ async def verify_received_pres( descriptor_map_list = pres.get("presentation_submission").get("descriptor_map") input_descriptors = pd.input_descriptors inp_desc_id_contraint_map = {} + inp_desc_id_schema_one_of_filter = set() for input_descriptor in input_descriptors: inp_desc_id_contraint_map[input_descriptor.id] = input_descriptor.constraint + if input_descriptor.schemas.oneOf: + inp_desc_id_schema_one_of_filter.add(input_descriptor.id) for desc_map_item in descriptor_map_list: desc_map_item_id = desc_map_item.get("id") constraint = inp_desc_id_contraint_map.get(desc_map_item_id) + is_one_of_filtered = False + if desc_map_item_id in inp_desc_id_schema_one_of_filter: + is_one_of_filtered = True desc_map_item_path = desc_map_item.get("path") jsonpath = parse(desc_map_item_path) match = jsonpath.find(pres) @@ -1367,15 +1384,27 @@ async def verify_received_pres( ) for match_item in match: if not await self.apply_constraint_received_cred( - constraint, match_item.value + constraint, match_item.value, is_one_of_filtered ): raise DIFPresExchError( f"Constraint specified for {desc_map_item_id} does not " f"apply to the enclosed credential in {desc_map_item_path}" ) + async def restrict_field_paths_one_of_filter( + self, field_paths: Sequence[str], cred_dict: dict + ) -> Sequence[str]: + """Return field_paths that are applicable to oneOf filter.""" + applied_field_paths = [] + for path in field_paths: + jsonpath = parse(path) + match = jsonpath.find(cred_dict) + if len(match) > 0: + applied_field_paths.append(path) + return applied_field_paths + async def apply_constraint_received_cred( - self, constraint: Constraints, cred_dict: dict + self, constraint: Constraints, cred_dict: dict, is_one_of_filtered: bool = False ) -> bool: """Evaluate constraint from the request against received credential.""" fields = constraint._fields @@ -1387,7 +1416,14 @@ async def apply_constraint_received_cred( field = await self.get_updated_field(field, cred_dict) if not await self.filter_by_field(field, credential): return False - field_paths = field_paths + field.paths + if is_one_of_filtered: + field_paths = field_paths + ( + await self.restrict_field_paths_one_of_filter( + field_paths=field.paths, cred_dict=cred_dict + ) + ) + else: + field_paths = field_paths + field.paths # Selective Disclosure check if is_limit_disclosure: field_paths = set([path.replace("$.", "") for path in field_paths]) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py index 2439cb07ee..8a6b10ac52 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py @@ -21,7 +21,7 @@ from ......wallet.base import BaseWallet from ......wallet.key_type import KeyType -from ....dif.pres_exch import PresentationDefinition +from ....dif.pres_exch import PresentationDefinition, SchemaInputDescriptor from ....dif.pres_exch_handler import DIFPresExchHandler from ....dif.pres_proposal_schema import DIFProofProposalSchema from ....dif.pres_request_schema import ( @@ -202,8 +202,11 @@ async def create_pres( one_of_uri_groups = [] if input_descriptor.schemas: if input_descriptor.schemas.oneOf: - for schema_uri_group in input_descriptor.schemas.uri_groups: - one_of_uri_groups.append(schema_uri_group) + one_of_uri_groups = ( + await self.retrieve_uri_list_from_schema_filter( + input_descriptor.schemas.uri_groups + ) + ) else: schema_uris = input_descriptor.schemas.uri_groups[0] for schema_uri in schema_uris: @@ -357,6 +360,19 @@ async def process_vcrecords_return_list( record_ids.add(vc_record.record_id) return (to_add, record_ids) + async def retrieve_uri_list_from_schema_filter( + self, schema_uri_groups: Sequence[Sequence[SchemaInputDescriptor]] + ) -> Sequence[str]: + """Retrieve list of schema uri from uri_group.""" + group_schema_uri_list = [] + for schema_group in schema_uri_groups: + uri_list = [] + for schema in schema_group: + uri_list.append(schema.uri) + if len(uri_list) > 0: + group_schema_uri_list.append(uri_list) + return group_schema_uri_list + async def receive_pres( self, message: V20Pres, pres_ex_record: V20PresExRecord ) -> None: diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py index 89466138bc..686b4979ee 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py @@ -3,6 +3,8 @@ from asynctest import mock as async_mock from marshmallow import ValidationError +from aries_cloudagent.protocols.present_proof.dif.pres_exch import SchemaInputDescriptor + from .......core.in_memory import InMemoryProfile from .......messaging.decorators.attach_decorator import AttachDecorator from .......storage.vc_holder.base import VCHolder @@ -1250,6 +1252,18 @@ async def test_process_vcrecords_return_list(self): 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 self.handler.retrieve_uri_list_from_schema_filter(test_schema_filter) + ) + assert test_one_of_uri_groups == [["test123", "test321"]] + async def test_verify_received_pres_a(self): dif_pres = V20Pres( formats=[ diff --git a/aries_cloudagent/protocols/present_proof/v2_0/routes.py b/aries_cloudagent/protocols/present_proof/v2_0/routes.py index f4c539eb88..5fa43e8e83 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/routes.py @@ -38,7 +38,7 @@ from ....vc.ld_proofs import BbsBlsSignature2020, Ed25519Signature2018 from ....wallet.error import WalletNotFoundError -from ..dif.pres_exch import InputDescriptors, ClaimFormat +from ..dif.pres_exch import InputDescriptors, ClaimFormat, SchemaInputDescriptor from ..dif.pres_proposal_schema import DIFProofProposalSchema from ..dif.pres_request_schema import ( DIFProofRequestSchema, @@ -514,8 +514,9 @@ async def present_proof_credentials_list(request: web.BaseRequest): one_of_uri_groups = [] if input_descriptor.schemas: if input_descriptor.schemas.oneOf: - for schema_uri_group in input_descriptor.schemas.uri_groups: - one_of_uri_groups.append(schema_uri_group) + one_of_uri_groups = await retrieve_uri_list_from_schema_filter( + input_descriptor.schemas.uri_groups + ) else: schema_uris = input_descriptor.schemas.uri_groups[0] for schema_uri in schema_uris: @@ -670,6 +671,20 @@ async def process_vcrecords_return_list( return (to_add, record_ids) +async def retrieve_uri_list_from_schema_filter( + schema_uri_groups: Sequence[Sequence[SchemaInputDescriptor]], +) -> Sequence[str]: + """Retrieve list of schema uri from uri_group.""" + group_schema_uri_list = [] + for schema_group in schema_uri_groups: + uri_list = [] + for schema in schema_group: + uri_list.append(schema.uri) + if len(uri_list) > 0: + group_schema_uri_list.append(uri_list) + return group_schema_uri_list + + @docs(tags=["present-proof v2.0"], summary="Sends a presentation proposal") @request_schema(V20PresProposalRequestSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py index 84bc940e0a..53b5007596 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py @@ -14,6 +14,8 @@ 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 @@ -2427,6 +2429,18 @@ async def test_process_vcrecords_return_list(self): 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 = async_mock.CoroutineMock(return_value={"comment": "test"}) self.request.match_info = { From a2fc4c070490c83ec1416625fe7799667fc6cff6 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Thu, 4 Nov 2021 20:04:48 -0700 Subject: [PATCH 27/29] oneOf filter update Signed-off-by: Shaanjot Gill --- .../present_proof/dif/pres_exch_handler.py | 3 + .../present_proof/dif/tests/test_data.py | 114 +++++++++++++++++- 2 files changed, 116 insertions(+), 1 deletion(-) 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 08e88dd671..235ad05be1 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -1015,6 +1015,9 @@ async def filter_schema( applicable = await self._credential_match_schema_filter_helper( credential=credential, filter=schemas.uri_groups ) + if applicable: + result.append(credential) + break else: uri_group = schemas.uri_groups[0] for schema in uri_group: 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 2a256983fc..cf74ca2c25 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py @@ -1302,6 +1302,117 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): """ pres_exch_multiple_srs_met = """ +{ + "id":"32f54163-7166-48f1-93d8-ff217bdb0653", + "submission_requirements":[ + { + "name": "Citizenship Information", + "rule": "all", + "from": "A" + }, + { + "name": "European Union Citizenship Proofs", + "rule": "all", + "from": "B" + }, + { + "name": "Date Test", + "rule": "all", + "from": "C" + } + ], + "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":{ + "fields":[ + { + "path":[ + "$.issuer.id", + "$.vc.issuer.id", + "$.issuer" + ], + "filter":{ + "type":"string", + "enum": ["did:example:489398593", "did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa", "did:sov:2wJPyULfLLnYTEFYzByfUR"] + } + } + ] + } + }, + { + "id":"citizenship_input_2", + "name":"US Passport", + "group":[ + "B" + ], + "schema":[ + { + "uri":"https://www.w3.org/2018/credentials#VerifiableCredential" + }, + { + "uri":"https://w3id.org/citizenship#PermanentResidentCard" + } + ], + "constraints":{ + "fields":[ + { + "path":[ + "$.credentialSubject.gender" + ], + "filter":{ + "const":"Male" + } + } + ] + } + }, + { + "id":"citizenship_input_3", + "name":"US Passport", + "group":[ + "C" + ], + "schema":[ + { + "uri":"https://www.w3.org/2018/credentials#VerifiableCredential" + }, + { + "uri":"https://w3id.org/citizenship#PermanentResidentCard" + } + ], + "constraints":{ + "fields":[ + { + "path":[ + "$.issuanceDate" + ], + "filter":{ + "type":"string", + "format":"date", + "minimum":"2005-5-16" + } + } + ] + } + } + ] +} +""" + +pres_exch_multiple_srs_met_one_of = """ { "id":"32f54163-7166-48f1-93d8-ff217bdb0653", "submission_requirements":[ @@ -2348,8 +2459,9 @@ def get_test_data(): pd_json_list = [ (pres_exch_multiple_srs_not_met, 0), (pres_exch_multiple_srs_met, 4), + (pres_exch_multiple_srs_met_one_of, 1), (pres_exch_datetime_minimum_met, 6), - (pres_exch_datetime_maximum_met, 6), + (pres_exch_datetime_maximum_met, 1), (pres_exch_nested_srs_a, 4), (pres_exch_nested_srs_b, 5), (pres_exch_nested_srs_c, 2), From afac7cbe3e13e7cff1d21ec1f6e2d9fddae34217 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Mon, 8 Nov 2021 08:16:35 -0800 Subject: [PATCH 28/29] updates Signed-off-by: Shaanjot Gill --- .../protocols/present_proof/dif/pres_exch.py | 37 ++++-- .../present_proof/dif/pres_exch_handler.py | 54 +++++---- .../present_proof/dif/tests/test_data.py | 19 ++-- .../present_proof/dif/tests/test_pres_exch.py | 10 +- .../dif/tests/test_pres_exch_handler.py | 4 +- .../present_proof/v2_0/formats/dif/handler.py | 2 +- .../v2_0/formats/dif/tests/test_handler.py | 107 +++++++++++++++++- .../protocols/present_proof/v2_0/routes.py | 2 +- .../present_proof/v2_0/tests/test_routes.py | 2 +- open-api/openapi.json | 6 - 10 files changed, 183 insertions(+), 60 deletions(-) diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py index 6e82905280..77a2b100d3 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py @@ -212,11 +212,11 @@ class Meta: def __init__( self, *, - oneOf: bool = False, + oneof_filter: bool = False, uri_groups: Sequence[Sequence[SchemaInputDescriptor]] = None, ): """Initialize SchemasInputDescriptorFilter.""" - self.oneOf = oneOf + self.oneof_filter = oneof_filter self.uri_groups = uri_groups @@ -230,17 +230,17 @@ class Meta: unknown = EXCLUDE uri_groups = fields.List(fields.List(fields.Nested(SchemaInputDescriptorSchema))) - oneOf = fields.Bool(description="oneOf") + oneof_filter = fields.Bool(description="oneOf") @pre_load def extract_info(self, data, **kwargs): """deserialize.""" new_data = {} if isinstance(data, dict): - if "oneOf" in data: - new_data["oneOf"] = True + if "oneof_filter" in data: + new_data["oneof_filter"] = True uri_group_list_of_list = [] - uri_group_list = data.get("oneOf") + uri_group_list = data.get("oneof_filter") for uri_group in uri_group_list: if isinstance(uri_group, list): uri_group_list_of_list.append(uri_group) @@ -248,7 +248,7 @@ def extract_info(self, data, **kwargs): uri_group_list_of_list.append([uri_group]) new_data["uri_groups"] = uri_group_list_of_list elif isinstance(data, list): - new_data["oneOf"] = False + new_data["oneof_filter"] = False new_data["uri_groups"] = [data] data = new_data return data @@ -638,7 +638,28 @@ class Meta: ConstraintsSchema, required=False, data_key="constraints" ) schemas = fields.Nested( - SchemasInputDescriptorFilterSchema, required=False, data_key="schema" + SchemasInputDescriptorFilterSchema, + required=False, + data_key="schema", + description=( + "Accepts a list of schema or a dict containing filters like oneof_filter." + ), + example=( + { + "oneOf": [ + [ + {"uri": "https://www.w3.org/Test1#Test1"}, + {"uri": "https://www.w3.org/Test2#Test2"}, + ], + { + "oneof_filter": [ + [{"uri": "https://www.w3.org/Test1#Test1"}], + [{"uri": "https://www.w3.org/Test2#Test2"}], + ] + }, + ] + } + ), ) 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 235ad05be1..8d33813f13 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -988,6 +988,13 @@ async def _credential_match_schema_filter_helper( ) if schema.required and not applicable: break + if applicable: + if schema.uri in [ + EXPANDED_TYPE_CREDENTIALS_CONTEXT_V1_VC_TYPE, + ]: + continue + else: + break return applicable async def filter_schema( @@ -1011,28 +1018,9 @@ async def filter_schema( result = [] for credential in credentials: applicable = False - if schemas.oneOf: - applicable = await self._credential_match_schema_filter_helper( - credential=credential, filter=schemas.uri_groups - ) - if applicable: - result.append(credential) - break - else: - uri_group = schemas.uri_groups[0] - for schema in uri_group: - applicable = self.credential_match_schema( - credential=credential, schema_id=schema.uri - ) - if schema.required and not applicable: - break - if applicable: - if schema.uri in [ - EXPANDED_TYPE_CREDENTIALS_CONTEXT_V1_VC_TYPE, - ]: - continue - else: - break + applicable = await self._credential_match_schema_filter_helper( + credential=credential, filter=schemas.uri_groups + ) if applicable: result.append(credential) return result @@ -1368,13 +1356,16 @@ async def verify_received_pres( input_descriptors = pd.input_descriptors inp_desc_id_contraint_map = {} inp_desc_id_schema_one_of_filter = set() + inp_desc_id_schemas_map = {} for input_descriptor in input_descriptors: inp_desc_id_contraint_map[input_descriptor.id] = input_descriptor.constraint - if input_descriptor.schemas.oneOf: + inp_desc_id_schemas_map[input_descriptor.id] = input_descriptor.schemas + if input_descriptor.schemas.oneof_filter: inp_desc_id_schema_one_of_filter.add(input_descriptor.id) for desc_map_item in descriptor_map_list: desc_map_item_id = desc_map_item.get("id") constraint = inp_desc_id_contraint_map.get(desc_map_item_id) + schema_filter = inp_desc_id_schemas_map.get(desc_map_item_id) is_one_of_filtered = False if desc_map_item_id in inp_desc_id_schema_one_of_filter: is_one_of_filtered = True @@ -1393,11 +1384,26 @@ async def verify_received_pres( f"Constraint specified for {desc_map_item_id} does not " f"apply to the enclosed credential in {desc_map_item_path}" ) + if ( + not len( + await self.filter_schema( + credentials=[ + self.create_vcrecord(cred_dict=match_item.value) + ], + schemas=schema_filter, + ) + ) + == 1 + ): + raise DIFPresExchError( + f"Schema filtering specified in {desc_map_item_id} does not " + f"match with the enclosed credential in {desc_map_item_path}" + ) async def restrict_field_paths_one_of_filter( self, field_paths: Sequence[str], cred_dict: dict ) -> Sequence[str]: - """Return field_paths that are applicable to oneOf filter.""" + """Return field_paths that are applicable to oneof_filter.""" applied_field_paths = [] for path in field_paths: jsonpath = parse(path) 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 cf74ca2c25..4ab8f602aa 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py @@ -1301,7 +1301,7 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): } """ -pres_exch_multiple_srs_met = """ +pres_exch_multiple_srs_met_one_of_filter_invalid = """ { "id":"32f54163-7166-48f1-93d8-ff217bdb0653", "submission_requirements":[ @@ -1330,10 +1330,7 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): ], "schema": [ { - "uri":"https://www.w3.org/2018/credentials#VerifiableCredential" - }, - { - "uri":"https://w3id.org/citizenship#PermanentResidentCard" + "uri":"https://www.w3.org/Test#Test" } ], "constraints":{ @@ -1412,7 +1409,7 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): } """ -pres_exch_multiple_srs_met_one_of = """ +pres_exch_multiple_srs_met_one_of_valid = """ { "id":"32f54163-7166-48f1-93d8-ff217bdb0653", "submission_requirements":[ @@ -1440,7 +1437,7 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): "A" ], "schema": { - "oneOf": [ + "oneof_filter": [ [ { "uri":"https://www.w3.org/2018/credentials#VerifiableCredential" @@ -1588,7 +1585,7 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): "A" ], "schema": { - "oneOf": [ + "oneof_filter": [ [ { "uri":"https://www.w3.org/2018/credentials#VerifiableCredential" @@ -2458,10 +2455,10 @@ def get_test_data(): ) pd_json_list = [ (pres_exch_multiple_srs_not_met, 0), - (pres_exch_multiple_srs_met, 4), - (pres_exch_multiple_srs_met_one_of, 1), + (pres_exch_multiple_srs_met_one_of_filter_invalid, 0), + (pres_exch_multiple_srs_met_one_of_valid, 4), (pres_exch_datetime_minimum_met, 6), - (pres_exch_datetime_maximum_met, 1), + (pres_exch_datetime_maximum_met, 6), (pres_exch_nested_srs_a, 4), (pres_exch_nested_srs_b, 5), (pres_exch_nested_srs_c, 2), 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 ca4155a79f..34638ef764 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 @@ -389,13 +389,13 @@ def test_schemas_input_desc_filter(self): [{"uri": "https://www.w3.org/Test#Test"}], ] test_schemas_filter = { - "oneOf": test_schema_list, + "oneof_filter": test_schema_list, } deser_schema_filter = SchemasInputDescriptorFilter.deserialize( test_schemas_filter ) - assert deser_schema_filter.oneOf + assert deser_schema_filter.oneof_filter assert deser_schema_filter.uri_groups[0][0].uri == test_schema_list[0][0].get( "uri" ) @@ -412,13 +412,13 @@ def test_schemas_input_desc_filter(self): {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, ] test_schemas_filter = { - "oneOf": test_schema_list, + "oneof_filter": test_schema_list, } deser_schema_filter = SchemasInputDescriptorFilter.deserialize( test_schemas_filter ) - assert deser_schema_filter.oneOf + assert deser_schema_filter.oneof_filter assert deser_schema_filter.uri_groups[0][0].uri == test_schema_list[0].get( "uri" ) @@ -428,7 +428,7 @@ def test_schemas_input_desc_filter(self): assert isinstance(deser_schema_filter, SchemasInputDescriptorFilter) deser_schema_filter = SchemasInputDescriptorFilter.deserialize(test_schema_list) - assert not deser_schema_filter.oneOf + assert not deser_schema_filter.oneof_filter assert deser_schema_filter.uri_groups[0][0].uri == test_schema_list[0].get( "uri" ) 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 1504f4adf0..9129f77b0e 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 @@ -1564,7 +1564,7 @@ async def test_filter_schema(self, setup_tuple, profile): cred_list, pd_list = setup_tuple dif_pres_exch_handler = DIFPresExchHandler(profile) tmp_schema_list = SchemasInputDescriptorFilter( - oneOf=True, + oneof_filter=True, uri_groups=[ [ SchemaInputDescriptor( @@ -3065,7 +3065,7 @@ async def test_multiple_applicable_creds_with_no_id(self, profile, setup_tuple): == "TEST" ) - tmp_pd = pd_list[1] + tmp_pd = pd_list[2] tmp_vp = await dif_pres_exch_handler.create_vp( credentials=test_creds, pd=tmp_pd[0], diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py index 8a6b10ac52..bf3de007ab 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py @@ -201,7 +201,7 @@ async def create_pres( uri_list = [] one_of_uri_groups = [] if input_descriptor.schemas: - if input_descriptor.schemas.oneOf: + if input_descriptor.schemas.oneof_filter: one_of_uri_groups = ( await self.retrieve_uri_list_from_schema_filter( input_descriptor.schemas.uri_groups diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py index 686b4979ee..b1c217acd3 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/tests/test_handler.py @@ -646,7 +646,7 @@ async def test_create_pres_one_of_filter(self): ] pres_request = deepcopy(DIF_PRES_REQUEST_B) pres_request["presentation_definition"]["input_descriptors"][0]["schema"] = { - "oneOf": [ + "oneof_filter": [ [ {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, @@ -1490,6 +1490,19 @@ async def test_verify_received_limit_disclosure_a(self): } ], } + pres_request["presentation_definition"]["input_descriptors"][0]["schema"] = { + "oneof_filter": [ + [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://www.vdel.com/MedicalPass"}, + {"uri": "http://hl7.org/fhir/Patient"}, + ], + [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + ] + } dif_pres_request = V20PresRequest( formats=[ V20PresFormat( @@ -1560,6 +1573,11 @@ async def test_verify_received_limit_disclosure_b(self): } ], } + pres_request["presentation_definition"]["input_descriptors"][0]["schema"] = [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://www.vdel.com/MedicalPass"}, + {"uri": "http://hl7.org/fhir/Patient"}, + ] dif_pres_request = V20PresRequest( formats=[ V20PresFormat( @@ -1830,3 +1848,90 @@ async def test_verify_received_pres_limit_disclosure_fail_b(self): ) with self.assertRaises(DIFPresExchError): await self.handler.receive_pres(message=dif_pres, pres_ex_record=record) + + async def test_verify_received_pres_fail_schema_filter(self): + dif_proof = deepcopy(DIF_PRES) + cred_dict = deepcopy(TEST_CRED_DICT) + cred_dict["credentialSubject"]["Patient"] = [ + { + "address": [ + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + ] + }, + { + "address": [ + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + { + "@id": "urn:bnid:_:c14n1", + "city": "Рума", + }, + ] + }, + ] + dif_proof["verifiableCredential"] = [] + dif_proof["verifiableCredential"].append(cred_dict) + dif_proof["verifiableCredential"].append(cred_dict) + dif_pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_json( + mapping=dif_proof, + ident="dif", + ) + ], + ) + pres_request = deepcopy(DIF_PRES_REQUEST_B) + pres_request["presentation_definition"]["input_descriptors"][0][ + "constraints" + ] = { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.Patient[0].address[*].city"], + "purpose": "Test", + } + ], + } + dif_pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_json(pres_request, ident="dif") + ], + ) + record = V20PresExRecord( + pres_ex_id="pxid", + thread_id="thid", + connection_id="conn_id", + initiator="init", + role="role", + state="state", + pres_request=dif_pres_request, + pres=dif_pres, + verified="false", + auto_present=True, + error_msg="error", + ) + with self.assertRaises(DIFPresExchError): + await self.handler.receive_pres(message=dif_pres, pres_ex_record=record) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/routes.py b/aries_cloudagent/protocols/present_proof/v2_0/routes.py index 5fa43e8e83..f233d9f07c 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/routes.py @@ -513,7 +513,7 @@ async def present_proof_credentials_list(request: web.BaseRequest): uri_list = [] one_of_uri_groups = [] if input_descriptor.schemas: - if input_descriptor.schemas.oneOf: + if input_descriptor.schemas.oneof_filter: one_of_uri_groups = await retrieve_uri_list_from_schema_filter( input_descriptor.schemas.uri_groups ) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py index 53b5007596..64d5cb2dad 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py @@ -496,7 +496,7 @@ async def test_present_proof_credentials_list_dif_one_of_filter(self): ) pres_request = deepcopy(DIF_PROOF_REQ) pres_request["presentation_definition"]["input_descriptors"][0]["schema"] = { - "oneOf": [ + "oneof_filter": [ [ {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, diff --git a/open-api/openapi.json b/open-api/openapi.json index 48e6b08666..a8eb2ab44a 100644 --- a/open-api/openapi.json +++ b/open-api/openapi.json @@ -7299,12 +7299,6 @@ "purpose" : { "type" : "string", "description" : "Purpose" - }, - "schema" : { - "type" : "array", - "items" : { - "$ref" : "#/definitions/SchemaInputDescriptor" - } } } }, From 3e16a87227420942c8689f73f993315236f901d8 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Mon, 8 Nov 2021 08:46:36 -0800 Subject: [PATCH 29/29] updates Signed-off-by: Shaanjot Gill --- .../protocols/present_proof/dif/pres_exch_handler.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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 8d33813f13..d4207ad6dd 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -1425,14 +1425,11 @@ async def apply_constraint_received_cred( field = await self.get_updated_field(field, cred_dict) if not await self.filter_by_field(field, credential): return False - if is_one_of_filtered: - field_paths = field_paths + ( - await self.restrict_field_paths_one_of_filter( - field_paths=field.paths, cred_dict=cred_dict - ) + field_paths = field_paths + ( + await self.restrict_field_paths_one_of_filter( + field_paths=field.paths, cred_dict=cred_dict ) - else: - field_paths = field_paths + field.paths + ) # Selective Disclosure check if is_limit_disclosure: field_paths = set([path.replace("$.", "") for path in field_paths])