From 3edd2f81f12fed4bd51d29316c6d05f696c60e99 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Wed, 24 Aug 2022 06:30:06 -0700 Subject: [PATCH 1/5] Remove aca-py check for unrevealed revealed attrs on proof validation Signed-off-by: Ian Costanzo --- aries_cloudagent/indy/verifier.py | 8 ++++++++ demo/runners/agent_container.py | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/indy/verifier.py b/aries_cloudagent/indy/verifier.py index f14ad14b24..db0a71bd93 100644 --- a/aries_cloudagent/indy/verifier.py +++ b/aries_cloudagent/indy/verifier.py @@ -159,6 +159,7 @@ async def check_timestamps( # timestamp superfluous, missing, or outside non-revocation interval revealed_attrs = pres["requested_proof"].get("revealed_attrs", {}) + unrevealed_attrs = pres["requested_proof"].get("unrevealed_attrs", {}) revealed_groups = pres["requested_proof"].get("revealed_attr_groups", {}) self_attested = pres["requested_proof"].get("self_attested_attrs", {}) preds = pres["requested_proof"].get("predicates", {}) @@ -190,6 +191,9 @@ async def check_timestamps( f"{uuid} falls outside non-revocation interval " f"{non_revoc_intervals[uuid]}" ) + elif uuid in unrevealed_attrs: + # nothing to do, attribute value is not revealed + pass elif uuid not in self_attested: raise ValueError( f"Presentation attributes mismatch requested attribute {uuid}" @@ -297,12 +301,16 @@ async def pre_verify(self, pres_req: dict, pres: dict): raise ValueError(f"Missing requested predicate '{uuid}'") revealed_attrs = pres["requested_proof"].get("revealed_attrs", {}) + unrevealed_attrs = pres["requested_proof"].get("unrevealed_attrs", {}) revealed_groups = pres["requested_proof"].get("revealed_attr_groups", {}) self_attested = pres["requested_proof"].get("self_attested_attrs", {}) for (uuid, req_attr) in pres_req["requested_attributes"].items(): if "name" in req_attr: if uuid in revealed_attrs: pres_req_attr_spec = {req_attr["name"]: revealed_attrs[uuid]} + elif uuid in unrevealed_attrs: + # unrevealed attribute, nothing to do + pres_req_attr_spec = {} elif uuid in self_attested: if not req_attr.get("restrictions"): continue diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index 065a3cb4ea..7018ee046f 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -331,14 +331,17 @@ async def handle_present_proof(self, message): if referent not in credentials_by_reft: credentials_by_reft[referent] = row + # submit the proof wit one unrevealed revealed attribute + revealed_flag = False for referent in presentation_request["requested_attributes"]: if referent in credentials_by_reft: revealed[referent] = { "cred_id": credentials_by_reft[referent]["cred_info"][ "referent" ], - "revealed": True, + "revealed": revealed_flag, } + revealed_flag = True else: self_attested[referent] = "my self-attested value" @@ -419,14 +422,17 @@ async def handle_present_proof_v2_0(self, message): if referent not in creds_by_reft: creds_by_reft[referent] = row + # submit the proof wit one unrevealed revealed attribute + revealed_flag = False for referent in pres_request_indy["requested_attributes"]: if referent in creds_by_reft: revealed[referent] = { "cred_id": creds_by_reft[referent]["cred_info"][ "referent" ], - "revealed": True, + "revealed": revealed_flag, } + revealed_flag = True else: self_attested[referent] = "my self-attested value" From 064a32d538ead727992d836bb88a717f1e60e774 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Wed, 24 Aug 2022 12:45:31 -0700 Subject: [PATCH 2/5] Proof validation status messages wip Signed-off-by: Ian Costanzo --- aries_cloudagent/indy/credx/verifier.py | 21 +++++--- aries_cloudagent/indy/sdk/verifier.py | 21 +++++--- aries_cloudagent/indy/verifier.py | 50 +++++++++++++++++-- .../protocols/present_proof/v1_0/manager.py | 22 ++++---- .../v1_0/models/presentation_exchange.py | 10 ++++ .../v2_0/formats/indy/handler.py | 18 +++---- .../v2_0/models/pres_exchange.py | 10 ++++ 7 files changed, 113 insertions(+), 39 deletions(-) diff --git a/aries_cloudagent/indy/credx/verifier.py b/aries_cloudagent/indy/credx/verifier.py index c6677cfa7b..309d0bfdaf 100644 --- a/aries_cloudagent/indy/credx/verifier.py +++ b/aries_cloudagent/indy/credx/verifier.py @@ -7,7 +7,7 @@ from ...core.profile import Profile -from ..verifier import IndyVerifier +from ..verifier import IndyVerifier, PresVerifyMsg LOGGER = logging.getLogger(__name__) @@ -33,7 +33,7 @@ async def verify_presentation( credential_definitions, rev_reg_defs, rev_reg_entries, - ) -> bool: + ) -> (bool, list): """ Verify a presentation. @@ -46,16 +46,20 @@ async def verify_presentation( rev_reg_entries: revocation registry entries """ + msgs = [] try: - self.non_revoc_intervals(pres_req, pres, credential_definitions) - await self.check_timestamps(self.profile, pres_req, pres, rev_reg_defs) - await self.pre_verify(pres_req, pres) + msgs += self.non_revoc_intervals(pres_req, pres, credential_definitions) + msgs += await self.check_timestamps(self.profile, pres_req, pres, rev_reg_defs) + msgs += await self.pre_verify(pres_req, pres) except ValueError as err: + msgs.append( + f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{err}" + ) LOGGER.error( f"Presentation on nonce={pres_req['nonce']} " f"cannot be validated: {str(err)}" ) - return False + return (False, msgs) try: presentation = Presentation.load(pres) @@ -69,10 +73,13 @@ async def verify_presentation( rev_reg_entries, ) except CredxError: + msgs.append( + f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{err}" + ) LOGGER.exception( f"Validation of presentation on nonce={pres_req['nonce']} " "failed with error" ) verified = False - return verified + return (verified, msgs) diff --git a/aries_cloudagent/indy/sdk/verifier.py b/aries_cloudagent/indy/sdk/verifier.py index b9e087aa82..409582a757 100644 --- a/aries_cloudagent/indy/sdk/verifier.py +++ b/aries_cloudagent/indy/sdk/verifier.py @@ -8,7 +8,7 @@ from ...core.profile import Profile -from ..verifier import IndyVerifier +from ..verifier import IndyVerifier, PresVerifyMsg LOGGER = logging.getLogger(__name__) @@ -34,7 +34,7 @@ async def verify_presentation( credential_definitions, rev_reg_defs, rev_reg_entries, - ) -> bool: + ) -> (bool, list): """ Verify a presentation. @@ -49,16 +49,20 @@ async def verify_presentation( LOGGER.debug(f">>> received presentation: {pres}") LOGGER.debug(f">>> for pres_req: {pres_req}") + msgs = [] try: - self.non_revoc_intervals(pres_req, pres, credential_definitions) - await self.check_timestamps(self.profile, pres_req, pres, rev_reg_defs) - await self.pre_verify(pres_req, pres) + msgs += self.non_revoc_intervals(pres_req, pres, credential_definitions) + msgs += await self.check_timestamps(self.profile, pres_req, pres, rev_reg_defs) + msgs += await self.pre_verify(pres_req, pres) except ValueError as err: + msgs.append( + f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{err}" + ) LOGGER.error( f"Presentation on nonce={pres_req['nonce']} " f"cannot be validated: {str(err)}" ) - return False + return (False, msgs) LOGGER.debug(f">>> verifying presentation: {pres}") LOGGER.debug(f">>> for pres_req: {pres_req}") @@ -72,10 +76,13 @@ async def verify_presentation( json.dumps(rev_reg_entries), ) except IndyError: + msgs.append( + f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{err}" + ) LOGGER.exception( f"Validation of presentation on nonce={pres_req['nonce']} " "failed with error" ) verified = False - return verified + return (verified, msgs) diff --git a/aries_cloudagent/indy/verifier.py b/aries_cloudagent/indy/verifier.py index db0a71bd93..0e2d260b84 100644 --- a/aries_cloudagent/indy/verifier.py +++ b/aries_cloudagent/indy/verifier.py @@ -3,6 +3,7 @@ import logging from abc import ABC, ABCMeta, abstractmethod +from enum import Enum from time import time from typing import Mapping @@ -16,8 +17,17 @@ from .models.xform import indy_proof_req2non_revoc_intervals + LOGGER = logging.getLogger(__name__) +class PresVerifyMsg(str, Enum): + RMV_REFERENT_NON_REVOC_INTERVAL = "RMV_RFNT_NRI" + RMV_GLOBAL_NON_REVOC_INTERVAL = "RMV_GLB_NRI" + TSTMP_OUT_NON_REVOC_INTRVAL = "TS_OUT_NRI" + CT_UNREVEALED_ATTRIBUTES = "UNRVL_ATTR" + PRES_VALUE_ERROR = "VALUE_ERROR" + PRES_VERIFY_ERROR = "VERIFY_ERROR" + class IndyVerifier(ABC, metaclass=ABCMeta): """Base class for Indy Verifier.""" @@ -32,7 +42,7 @@ def __repr__(self) -> str: """ return "<{}>".format(self.__class__.__name__) - def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict): + def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict) -> list: """ Remove superfluous non-revocation intervals in presentation request. @@ -45,6 +55,7 @@ def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict): pres: corresponding presentation """ + msgs = [] for (req_proof_key, pres_key) in { "revealed_attrs": "requested_attributes", "revealed_attr_groups": "requested_attributes", @@ -60,6 +71,10 @@ def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict): if uuid in pres_req[pres_key] and pres_req[pres_key][uuid].pop( "non_revoked", None ): + msgs.append( + f"{PresVerifyMsg.RMV_REFERENT_NON_REVOC_INTERVAL.value}::" + f"{uuid}" + ) LOGGER.info( ( "Amended presentation request (nonce=%s): removed " @@ -79,6 +94,7 @@ def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict): for spec in pres["identifiers"] ): pres_req.pop("non_revoked", None) + msgs.append(PresVerifyMsg.RMV_GLOBAL_NON_REVOC_INTERVAL.value) LOGGER.warning( ( "Amended presentation request (nonce=%s); removed global " @@ -86,6 +102,7 @@ def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict): ), pres_req["nonce"], ) + return msgs async def check_timestamps( self, @@ -93,7 +110,7 @@ async def check_timestamps( pres_req: Mapping, pres: Mapping, rev_reg_defs: Mapping, - ): + ) -> list: """ Check for suspicious, missing, and superfluous timestamps. @@ -106,6 +123,7 @@ async def check_timestamps( pres: indy proof request rev_reg_defs: rev reg defs by rev reg id, augmented with transaction times """ + msgs = [] now = int(time()) non_revoc_intervals = indy_proof_req2non_revoc_intervals(pres_req) LOGGER.debug(f">>> got non-revoc intervals: {non_revoc_intervals}") @@ -186,6 +204,10 @@ async def check_timestamps( < timestamp < non_revoc_intervals[uuid].get("to", now) ): + msgs.append( + f"{PresVerifyMsg.TSTMP_OUT_NON_REVOC_INTRVAL.value}::" + f"{uuid}" + ) LOGGER.info( f"Timestamp {timestamp} from ledger for item" f"{uuid} falls outside non-revocation interval " @@ -193,7 +215,10 @@ async def check_timestamps( ) elif uuid in unrevealed_attrs: # nothing to do, attribute value is not revealed - pass + msgs.append( + f"{PresVerifyMsg.CT_UNREVEALED_ATTRIBUTES.value}::" + f"{uuid}" + ) elif uuid not in self_attested: raise ValueError( f"Presentation attributes mismatch requested attribute {uuid}" @@ -221,6 +246,10 @@ async def check_timestamps( < timestamp < non_revoc_intervals[uuid].get("to", now) ): + msgs.append( + f"{PresVerifyMsg.TSTMP_OUT_NON_REVOC_INTRVAL.value}::" + f"{uuid}" + ) LOGGER.warning( f"Timestamp {timestamp} from ledger for item" f"{uuid} falls outside non-revocation interval " @@ -247,13 +276,18 @@ async def check_timestamps( < timestamp < non_revoc_intervals[uuid].get("to", now) ): + msgs.append( + f"{PresVerifyMsg.TSTMP_OUT_NON_REVOC_INTRVAL.value}::" + f"{uuid}" + ) LOGGER.warning( f"Best-effort timestamp {timestamp} " "from ledger falls outside non-revocation interval " f"{non_revoc_intervals[uuid]}" ) + return msgs - async def pre_verify(self, pres_req: dict, pres: dict): + async def pre_verify(self, pres_req: dict, pres: dict) -> list: """ Check for essential components and tampering in presentation. @@ -265,6 +299,7 @@ async def pre_verify(self, pres_req: dict, pres: dict): pres: corresponding presentation """ + msgs = [] if not ( pres_req and "requested_predicates" in pres_req @@ -311,6 +346,10 @@ async def pre_verify(self, pres_req: dict, pres: dict): elif uuid in unrevealed_attrs: # unrevealed attribute, nothing to do pres_req_attr_spec = {} + msgs.append( + f"{PresVerifyMsg.CT_UNREVEALED_ATTRIBUTES.value}::" + f"{uuid}" + ) elif uuid in self_attested: if not req_attr.get("restrictions"): continue @@ -347,6 +386,7 @@ async def pre_verify(self, pres_req: dict, pres: dict): raise ValueError(f"Encoded representation mismatch for '{attr}'") if primary_enco != encode(spec["raw"]): raise ValueError(f"Encoded representation mismatch for '{attr}'") + return msgs @abstractmethod def verify_presentation( @@ -357,7 +397,7 @@ def verify_presentation( credential_definitions, rev_reg_defs, rev_reg_entries, - ): + ) -> (bool, list): """ Verify a presentation. diff --git a/aries_cloudagent/protocols/present_proof/v1_0/manager.py b/aries_cloudagent/protocols/present_proof/v1_0/manager.py index c951aa8c02..b8ae25b122 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/manager.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/manager.py @@ -417,18 +417,18 @@ async def verify_presentation( ) = await indy_handler.process_pres_identifiers(indy_proof["identifiers"]) verifier = self._profile.inject(IndyVerifier) - presentation_exchange_record.verified = json.dumps( # tag: needs string value - await verifier.verify_presentation( - dict( - indy_proof_request - ), # copy to avoid changing the proof req in the stored pres exch - indy_proof, - schemas, - cred_defs, - rev_reg_defs, - rev_reg_entries, - ) + (verified_bool, verified_msgs) = await verifier.verify_presentation( + dict( + indy_proof_request + ), # copy to avoid changing the proof req in the stored pres exch + indy_proof, + schemas, + cred_defs, + rev_reg_defs, + rev_reg_entries, ) + presentation_exchange_record.verified = json.dumps(verified_bool) + presentation_exchange_record.verified_msgs = verified_msgs presentation_exchange_record.state = V10PresentationExchange.STATE_VERIFIED async with self._profile.session() as session: diff --git a/aries_cloudagent/protocols/present_proof/v1_0/models/presentation_exchange.py b/aries_cloudagent/protocols/present_proof/v1_0/models/presentation_exchange.py index 296740b5f9..80db45f86c 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/models/presentation_exchange.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/models/presentation_exchange.py @@ -74,6 +74,7 @@ def __init__( ] = None, # aries message presentation: Union[IndyProof, Mapping] = None, # indy proof verified: str = None, + verified_msgs: list = None, auto_present: bool = False, auto_verify: bool = False, error_msg: str = None, @@ -96,6 +97,7 @@ def __init__( ) self._presentation = IndyProof.serde(presentation) self.verified = verified + self.verified_msgs = verified_msgs self.auto_present = auto_present self.auto_verify = auto_verify self.error_msg = error_msg @@ -208,6 +210,7 @@ def record_value(self) -> Mapping: "auto_verify", "error_msg", "verified", + "verified_msgs", "trace", ) }, @@ -295,6 +298,13 @@ class Meta: example="true", validate=validate.OneOf(["true", "false"]), ) + verified_msgs = fields.List( + fields.Str( + required=False, + description="Proof verification warning or error information", + ), + required=False, + ) auto_present = fields.Bool( required=False, description="Prover choice to auto-present proof as verifier requests", diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py index 54ef915af0..3bb2eaf1ee 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py @@ -329,14 +329,14 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: ) = await indy_handler.process_pres_identifiers(indy_proof["identifiers"]) verifier = self._profile.inject(IndyVerifier) - pres_ex_record.verified = json.dumps( # tag: needs string value - await verifier.verify_presentation( - indy_proof_request, - indy_proof, - schemas, - cred_defs, - rev_reg_defs, - rev_reg_entries, - ) + (verified, verified_msgs) = await verifier.verify_presentation( + indy_proof_request, + indy_proof, + schemas, + cred_defs, + rev_reg_defs, + rev_reg_entries, ) + pres_ex_record.verified = json.dumps(verified) + pres_ex_record.verified_msgs = verified_msgs return pres_ex_record diff --git a/aries_cloudagent/protocols/present_proof/v2_0/models/pres_exchange.py b/aries_cloudagent/protocols/present_proof/v2_0/models/pres_exchange.py index f77a53166b..cc314aa9db 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/models/pres_exchange.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/models/pres_exchange.py @@ -62,6 +62,7 @@ def __init__( pres_request: Union[V20PresRequest, Mapping] = None, # aries message pres: Union[V20Pres, Mapping] = None, # aries message verified: str = None, + verified_msgs: list = None, auto_present: bool = False, auto_verify: bool = False, error_msg: str = None, @@ -80,6 +81,7 @@ def __init__( self._pres_request = V20PresRequest.serde(pres_request) self._pres = V20Pres.serde(pres) self.verified = verified + self.verified_msgs = verified_msgs self.auto_present = auto_present self.auto_verify = auto_verify self.error_msg = error_msg @@ -191,6 +193,7 @@ def record_value(self) -> Mapping: "role", "state", "verified", + "verified_msgs", "auto_present", "auto_verify", "error_msg", @@ -307,6 +310,13 @@ class Meta: example="true", validate=validate.OneOf(["true", "false"]), ) + verified_msgs = fields.List( + fields.Str( + required=False, + description="Proof verification warning or error information", + ), + required=False, + ) auto_present = fields.Bool( required=False, description="Prover choice to auto-present proof as verifier requests", From e5bc71ec211553adc5e2cca2d33e4f4569efbd87 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Thu, 25 Aug 2022 13:07:43 -0700 Subject: [PATCH 3/5] Fix up unit tests Signed-off-by: Ian Costanzo --- aries_cloudagent/indy/credx/verifier.py | 16 +++---- .../indy/sdk/tests/test_verifier.py | 44 +++++++++++++++---- aries_cloudagent/indy/sdk/verifier.py | 16 +++---- aries_cloudagent/indy/verifier.py | 12 ++--- .../protocols/present_proof/v1_0/manager.py | 2 +- .../v1_0/models/tests/test_record.py | 1 + .../present_proof/v1_0/tests/test_manager.py | 2 +- .../v2_0/formats/indy/handler.py | 2 +- .../v2_0/models/tests/test_record.py | 1 + .../present_proof/v2_0/tests/test_manager.py | 2 +- 10 files changed, 63 insertions(+), 35 deletions(-) diff --git a/aries_cloudagent/indy/credx/verifier.py b/aries_cloudagent/indy/credx/verifier.py index 309d0bfdaf..e625076ecd 100644 --- a/aries_cloudagent/indy/credx/verifier.py +++ b/aries_cloudagent/indy/credx/verifier.py @@ -49,12 +49,13 @@ async def verify_presentation( msgs = [] try: msgs += self.non_revoc_intervals(pres_req, pres, credential_definitions) - msgs += await self.check_timestamps(self.profile, pres_req, pres, rev_reg_defs) + msgs += await self.check_timestamps( + self.profile, pres_req, pres, rev_reg_defs + ) msgs += await self.pre_verify(pres_req, pres) except ValueError as err: - msgs.append( - f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{err}" - ) + s = str(err) + msgs.append(f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{s}") LOGGER.error( f"Presentation on nonce={pres_req['nonce']} " f"cannot be validated: {str(err)}" @@ -72,10 +73,9 @@ async def verify_presentation( rev_reg_defs.values(), rev_reg_entries, ) - except CredxError: - msgs.append( - f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{err}" - ) + except CredxError as err: + s = str(err) + msgs.append(f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{s}") LOGGER.exception( f"Validation of presentation on nonce={pres_req['nonce']} " "failed with error" diff --git a/aries_cloudagent/indy/sdk/tests/test_verifier.py b/aries_cloudagent/indy/sdk/tests/test_verifier.py index 44784b1ad5..0f1cbef3ab 100644 --- a/aries_cloudagent/indy/sdk/tests/test_verifier.py +++ b/aries_cloudagent/indy/sdk/tests/test_verifier.py @@ -336,7 +336,7 @@ async def test_verify_presentation(self, mock_verify): ) as mock_get_ledger: mock_get_ledger.return_value = (None, self.ledger) INDY_PROOF_REQ_X = deepcopy(INDY_PROOF_REQ_PRED_NAMES) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_X, INDY_PROOF_PRED_NAMES, "schemas", @@ -370,7 +370,7 @@ async def test_verify_presentation_x_indy(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = ("test", self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_NAME, INDY_PROOF_NAME, "schemas", @@ -397,7 +397,7 @@ async def test_check_encoding_attr(self, mock_verify): ) as mock_get_ledger: mock_get_ledger.return_value = (None, self.ledger) mock_verify.return_value = True - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_NAME, INDY_PROOF_NAME, "schemas", @@ -415,6 +415,8 @@ async def test_check_encoding_attr(self, mock_verify): json.dumps("rev_reg_entries"), ) assert verified is True + assert len(msgs) == 1 + assert "TS_OUT_NRI::19_uuid" in msgs @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_encoding_attr_tamper_raw(self, mock_verify): @@ -426,7 +428,7 @@ async def test_check_encoding_attr_tamper_raw(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = ("test", self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_NAME, INDY_PROOF_X, "schemas", @@ -438,6 +440,9 @@ async def test_check_encoding_attr_tamper_raw(self, mock_verify): mock_verify.assert_not_called() assert verified is False + assert len(msgs) == 2 + assert "TS_OUT_NRI::19_uuid" in msgs + assert "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_encoding_attr_tamper_encoded(self, mock_verify): @@ -449,7 +454,7 @@ async def test_check_encoding_attr_tamper_encoded(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = (None, self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_NAME, INDY_PROOF_X, "schemas", @@ -461,6 +466,9 @@ async def test_check_encoding_attr_tamper_encoded(self, mock_verify): mock_verify.assert_not_called() assert verified is False + assert len(msgs) == 2 + assert "TS_OUT_NRI::19_uuid" in msgs + assert "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names(self, mock_verify): @@ -470,7 +478,7 @@ async def test_check_pred_names(self, mock_verify): mock_get_ledger.return_value = ("test", self.ledger) mock_verify.return_value = True INDY_PROOF_REQ_X = deepcopy(INDY_PROOF_REQ_PRED_NAMES) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_X, INDY_PROOF_PRED_NAMES, "schemas", @@ -491,6 +499,10 @@ async def test_check_pred_names(self, mock_verify): ) assert verified is True + assert len(msgs) == 3 + assert "TS_OUT_NRI::18_uuid" in msgs + assert "TS_OUT_NRI::18_id_GE_uuid" in msgs + assert "TS_OUT_NRI::18_busid_GE_uuid" in msgs @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names_tamper_pred_value(self, mock_verify): @@ -502,7 +514,7 @@ async def test_check_pred_names_tamper_pred_value(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = (None, self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( deepcopy(INDY_PROOF_REQ_PRED_NAMES), INDY_PROOF_X, "schemas", @@ -514,6 +526,11 @@ async def test_check_pred_names_tamper_pred_value(self, mock_verify): mock_verify.assert_not_called() assert verified is False + assert len(msgs) == 4 + assert "RMV_RFNT_NRI::18_uuid" in msgs + assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs + assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs + assert "VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid" in msgs @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify): @@ -523,7 +540,7 @@ async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = (None, self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( INDY_PROOF_REQ_X, INDY_PROOF_PRED_NAMES, "schemas", @@ -535,6 +552,11 @@ async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify): mock_verify.assert_not_called() assert verified is False + assert len(msgs) == 4 + assert "RMV_RFNT_NRI::18_uuid" in msgs + assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs + assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs + assert "VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid" in msgs @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names_tamper_attr_groups(self, mock_verify): @@ -546,7 +568,7 @@ async def test_check_pred_names_tamper_attr_groups(self, mock_verify): IndyLedgerRequestsExecutor, "get_ledger_for_identifier" ) as mock_get_ledger: mock_get_ledger.return_value = ("test", self.ledger) - verified = await self.verifier.verify_presentation( + (verified, msgs) = await self.verifier.verify_presentation( deepcopy(INDY_PROOF_REQ_PRED_NAMES), INDY_PROOF_X, "schemas", @@ -558,3 +580,7 @@ async def test_check_pred_names_tamper_attr_groups(self, mock_verify): mock_verify.assert_not_called() assert verified is False + assert len(msgs) == 3 + assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs + assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs + assert "VALUE_ERROR::Missing requested attribute group 18_uuid" in msgs diff --git a/aries_cloudagent/indy/sdk/verifier.py b/aries_cloudagent/indy/sdk/verifier.py index 409582a757..5c67463eed 100644 --- a/aries_cloudagent/indy/sdk/verifier.py +++ b/aries_cloudagent/indy/sdk/verifier.py @@ -52,12 +52,13 @@ async def verify_presentation( msgs = [] try: msgs += self.non_revoc_intervals(pres_req, pres, credential_definitions) - msgs += await self.check_timestamps(self.profile, pres_req, pres, rev_reg_defs) + msgs += await self.check_timestamps( + self.profile, pres_req, pres, rev_reg_defs + ) msgs += await self.pre_verify(pres_req, pres) except ValueError as err: - msgs.append( - f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{err}" - ) + s = str(err) + msgs.append(f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{s}") LOGGER.error( f"Presentation on nonce={pres_req['nonce']} " f"cannot be validated: {str(err)}" @@ -75,10 +76,9 @@ async def verify_presentation( json.dumps(rev_reg_defs), json.dumps(rev_reg_entries), ) - except IndyError: - msgs.append( - f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{err}" - ) + except IndyError as err: + s = str(err) + msgs.append(f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{s}") LOGGER.exception( f"Validation of presentation on nonce={pres_req['nonce']} " "failed with error" diff --git a/aries_cloudagent/indy/verifier.py b/aries_cloudagent/indy/verifier.py index 0e2d260b84..f61ca829f2 100644 --- a/aries_cloudagent/indy/verifier.py +++ b/aries_cloudagent/indy/verifier.py @@ -20,7 +20,10 @@ LOGGER = logging.getLogger(__name__) + class PresVerifyMsg(str, Enum): + """Credential verification codes.""" + RMV_REFERENT_NON_REVOC_INTERVAL = "RMV_RFNT_NRI" RMV_GLOBAL_NON_REVOC_INTERVAL = "RMV_GLB_NRI" TSTMP_OUT_NON_REVOC_INTRVAL = "TS_OUT_NRI" @@ -216,8 +219,7 @@ async def check_timestamps( elif uuid in unrevealed_attrs: # nothing to do, attribute value is not revealed msgs.append( - f"{PresVerifyMsg.CT_UNREVEALED_ATTRIBUTES.value}::" - f"{uuid}" + f"{PresVerifyMsg.CT_UNREVEALED_ATTRIBUTES.value}::" f"{uuid}" ) elif uuid not in self_attested: raise ValueError( @@ -277,8 +279,7 @@ async def check_timestamps( < non_revoc_intervals[uuid].get("to", now) ): msgs.append( - f"{PresVerifyMsg.TSTMP_OUT_NON_REVOC_INTRVAL.value}::" - f"{uuid}" + f"{PresVerifyMsg.TSTMP_OUT_NON_REVOC_INTRVAL.value}::" f"{uuid}" ) LOGGER.warning( f"Best-effort timestamp {timestamp} " @@ -347,8 +348,7 @@ async def pre_verify(self, pres_req: dict, pres: dict) -> list: # unrevealed attribute, nothing to do pres_req_attr_spec = {} msgs.append( - f"{PresVerifyMsg.CT_UNREVEALED_ATTRIBUTES.value}::" - f"{uuid}" + f"{PresVerifyMsg.CT_UNREVEALED_ATTRIBUTES.value}::" f"{uuid}" ) elif uuid in self_attested: if not req_attr.get("restrictions"): diff --git a/aries_cloudagent/protocols/present_proof/v1_0/manager.py b/aries_cloudagent/protocols/present_proof/v1_0/manager.py index b8ae25b122..2f3af46da5 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/manager.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/manager.py @@ -428,7 +428,7 @@ async def verify_presentation( rev_reg_entries, ) presentation_exchange_record.verified = json.dumps(verified_bool) - presentation_exchange_record.verified_msgs = verified_msgs + presentation_exchange_record.verified_msgs = list(set(verified_msgs)) presentation_exchange_record.state = V10PresentationExchange.STATE_VERIFIED async with self._profile.session() as session: diff --git a/aries_cloudagent/protocols/present_proof/v1_0/models/tests/test_record.py b/aries_cloudagent/protocols/present_proof/v1_0/models/tests/test_record.py index a757dcae2b..13d9e5aac3 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/models/tests/test_record.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/models/tests/test_record.py @@ -113,6 +113,7 @@ async def test_record(self): "auto_verify": False, "error_msg": None, "verified": None, + "verified_msgs": None, "trace": False, } diff --git a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py index 310cc4421f..044c21d317 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py @@ -317,7 +317,7 @@ async def setUp(self): Verifier = async_mock.MagicMock(IndyVerifier, autospec=True) self.verifier = Verifier() self.verifier.verify_presentation = async_mock.CoroutineMock( - return_value="true" + return_value=("true", []) ) injector.bind_instance(IndyVerifier, self.verifier) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py index 3bb2eaf1ee..8f0a3e5057 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py @@ -338,5 +338,5 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: rev_reg_entries, ) pres_ex_record.verified = json.dumps(verified) - pres_ex_record.verified_msgs = verified_msgs + pres_ex_record.verified_msgs = list(set(verified_msgs)) return pres_ex_record diff --git a/aries_cloudagent/protocols/present_proof/v2_0/models/tests/test_record.py b/aries_cloudagent/protocols/present_proof/v2_0/models/tests/test_record.py index 529b72bb6a..c22a6ff23b 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/models/tests/test_record.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/models/tests/test_record.py @@ -110,6 +110,7 @@ async def test_record(self): "state": "state", "pres_proposal": pres_proposal.serialize(), "verified": "false", + "verified_msgs": None, "auto_present": True, "auto_verify": False, "error_msg": "error", diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py index 0e352c51e2..9081c61946 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py @@ -476,7 +476,7 @@ async def setUp(self): Verifier = async_mock.MagicMock(IndyVerifier, autospec=True) self.verifier = Verifier() self.verifier.verify_presentation = async_mock.CoroutineMock( - return_value="true" + return_value=("true", []) ) injector.bind_instance(IndyVerifier, self.verifier) From c5f384d503591c5376e3506e656515fa87c774b7 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Thu, 25 Aug 2022 13:47:59 -0700 Subject: [PATCH 4/5] Add some proof verification docs Signed-off-by: Ian Costanzo --- AnoncredsProofValidation.md | 84 +++++++++++++++++++ .../indy/sdk/tests/test_verifier.py | 18 +++- 2 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 AnoncredsProofValidation.md diff --git a/AnoncredsProofValidation.md b/AnoncredsProofValidation.md new file mode 100644 index 0000000000..3f5c0e138e --- /dev/null +++ b/AnoncredsProofValidation.md @@ -0,0 +1,84 @@ +# Anoncreds Proof Validation in Aca-Py + +Aca-Py does some pre-validation when verifying Anoncreds presentations (proofs), some scenarios are rejected (things that are indicative of tampering, for example) and some attributes are removed before running the anoncreds validation (for example removing superfluous non-revocation timestamps). Any Aca-Py validations or presentation modifications are indicated by the "verify_msgs" attribute in the final presentation exchange object + +The list of possible verification messages is [here](https://github.com/hyperledger/aries-cloudagent-python/blob/main/aries_cloudagent/indy/verifier.py#L24), and consists of: + +``` +class PresVerifyMsg(str, Enum): + """Credential verification codes.""" + + RMV_REFERENT_NON_REVOC_INTERVAL = "RMV_RFNT_NRI" + RMV_GLOBAL_NON_REVOC_INTERVAL = "RMV_GLB_NRI" + TSTMP_OUT_NON_REVOC_INTRVAL = "TS_OUT_NRI" + CT_UNREVEALED_ATTRIBUTES = "UNRVL_ATTR" + PRES_VALUE_ERROR = "VALUE_ERROR" + PRES_VERIFY_ERROR = "VERIFY_ERROR" +``` + +If there is additional information, it will be included like this: `TS_OUT_NRI::19_uuid` (which means the attribute identified by `19_uuid` contained a timestamp outside of the non-revocation interval (which is just a warning)). + +A presentation verification may include multiple messages, for example: + +``` + ... + "verified": "true", + "verified_msgs": [ + "TS_OUT_NRI::18_uuid", + "TS_OUT_NRI::18_id_GE_uuid", + "TS_OUT_NRI::18_busid_GE_uuid" + ], + ... +``` + +... or it may include a single message, for example: + +``` + ... + "verified": "false", + "verified_msgs": [ + "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" + ], + ... +``` + +... or the `verified_msgs` may be null or an empty array. + +## Presentation Modifications and Warnings + +The following modifications/warnings may be done by Aca-Py 9which shouldn't affect the verification of the received proof): + +- "RMV_RFNT_NRI": Referent contains a non-revocation interval for a non-revocable credential (timestamp is removed) +- "RMV_GLB_NRI": Presentation contains a global interval for a non-revocable credential (timestamp is removed) +- "TS_OUT_NRI": Presentation contains a non-revocation timestamp outside of the requested non-revocation interval (warning) +- "UNRVL_ATTR": Presentation contains attributes with unrevealed values (warning) + +## Presentation Pre-validation Errors + +The following pre-verification checks are done, which will fail the proof (before calling anoncreds) and will result in the following message: + +``` +VALUE_ERROR:: +``` + +These validations are all done within the [Indy verifier class](https://github.com/hyperledger/aries-cloudagent-python/blob/main/aries_cloudagent/indy/verifier.py) - to see the detailed validation just look for anywhere a `raise ValueError(...)` appears in the code. + +A summary of the possible errors is: + +- information missing in presentation exchange record +- timestamp provided for irrevocable credential +- referenced revocation registry not found on ledger +- timestamp outside of reasonable range (future date or pre-dates revocation registry) +- mis-match between provided and requested timestamps for non-revocation +- mis-match between requested and provided attributes or predicates +- self-attested attribute is provided for a requested attribute with restrictions +- encoded value doesn't match raw value + +## Anoncreds Verification Exceptions + +Typically when you call the anoncreds `verifier_verify_proof()` method, it will return a `True` or `False` based on whether the presentation cryptographically verifies. However in the case where anoncreds throws an exception, the exception text will be included in a verification message as follows: + +``` +VERIFY_ERROR:: +``` + diff --git a/aries_cloudagent/indy/sdk/tests/test_verifier.py b/aries_cloudagent/indy/sdk/tests/test_verifier.py index 0f1cbef3ab..d4abc1bdd1 100644 --- a/aries_cloudagent/indy/sdk/tests/test_verifier.py +++ b/aries_cloudagent/indy/sdk/tests/test_verifier.py @@ -442,7 +442,9 @@ async def test_check_encoding_attr_tamper_raw(self, mock_verify): assert verified is False assert len(msgs) == 2 assert "TS_OUT_NRI::19_uuid" in msgs - assert "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs + assert ( + "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs + ) @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_encoding_attr_tamper_encoded(self, mock_verify): @@ -468,7 +470,9 @@ async def test_check_encoding_attr_tamper_encoded(self, mock_verify): assert verified is False assert len(msgs) == 2 assert "TS_OUT_NRI::19_uuid" in msgs - assert "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs + assert ( + "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs + ) @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names(self, mock_verify): @@ -530,7 +534,10 @@ async def test_check_pred_names_tamper_pred_value(self, mock_verify): assert "RMV_RFNT_NRI::18_uuid" in msgs assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs - assert "VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid" in msgs + assert ( + "VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid" + in msgs + ) @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify): @@ -556,7 +563,10 @@ async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify): assert "RMV_RFNT_NRI::18_uuid" in msgs assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs - assert "VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid" in msgs + assert ( + "VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid" + in msgs + ) @async_mock.patch("indy.anoncreds.verifier_verify_proof") async def test_check_pred_names_tamper_attr_groups(self, mock_verify): From e9d4f9a73ba3f056e3747c0391927544e3e86d85 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Thu, 25 Aug 2022 14:40:11 -0700 Subject: [PATCH 5/5] Fix typo Signed-off-by: Ian Costanzo --- AnoncredsProofValidation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AnoncredsProofValidation.md b/AnoncredsProofValidation.md index 3f5c0e138e..9114c3a727 100644 --- a/AnoncredsProofValidation.md +++ b/AnoncredsProofValidation.md @@ -46,7 +46,7 @@ A presentation verification may include multiple messages, for example: ## Presentation Modifications and Warnings -The following modifications/warnings may be done by Aca-Py 9which shouldn't affect the verification of the received proof): +The following modifications/warnings may be done by Aca-Py which shouldn't affect the verification of the received proof): - "RMV_RFNT_NRI": Referent contains a non-revocation interval for a non-revocable credential (timestamp is removed) - "RMV_GLB_NRI": Presentation contains a global interval for a non-revocable credential (timestamp is removed)