Skip to content

Commit 5a636d6

Browse files
authored
Merge pull request openwallet-foundation#1913 from ianco/fix/proof-unrevealed-attrs
Remove aca-py check for unrevealed revealed attrs on proof validation
2 parents cbd5cee + 79692de commit 5a636d6

14 files changed

Lines changed: 263 additions & 53 deletions

File tree

AnoncredsProofValidation.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Anoncreds Proof Validation in Aca-Py
2+
3+
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
4+
5+
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:
6+
7+
```
8+
class PresVerifyMsg(str, Enum):
9+
"""Credential verification codes."""
10+
11+
RMV_REFERENT_NON_REVOC_INTERVAL = "RMV_RFNT_NRI"
12+
RMV_GLOBAL_NON_REVOC_INTERVAL = "RMV_GLB_NRI"
13+
TSTMP_OUT_NON_REVOC_INTRVAL = "TS_OUT_NRI"
14+
CT_UNREVEALED_ATTRIBUTES = "UNRVL_ATTR"
15+
PRES_VALUE_ERROR = "VALUE_ERROR"
16+
PRES_VERIFY_ERROR = "VERIFY_ERROR"
17+
```
18+
19+
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)).
20+
21+
A presentation verification may include multiple messages, for example:
22+
23+
```
24+
...
25+
"verified": "true",
26+
"verified_msgs": [
27+
"TS_OUT_NRI::18_uuid",
28+
"TS_OUT_NRI::18_id_GE_uuid",
29+
"TS_OUT_NRI::18_busid_GE_uuid"
30+
],
31+
...
32+
```
33+
34+
... or it may include a single message, for example:
35+
36+
```
37+
...
38+
"verified": "false",
39+
"verified_msgs": [
40+
"VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'"
41+
],
42+
...
43+
```
44+
45+
... or the `verified_msgs` may be null or an empty array.
46+
47+
## Presentation Modifications and Warnings
48+
49+
The following modifications/warnings may be done by Aca-Py which shouldn't affect the verification of the received proof):
50+
51+
- "RMV_RFNT_NRI": Referent contains a non-revocation interval for a non-revocable credential (timestamp is removed)
52+
- "RMV_GLB_NRI": Presentation contains a global interval for a non-revocable credential (timestamp is removed)
53+
- "TS_OUT_NRI": Presentation contains a non-revocation timestamp outside of the requested non-revocation interval (warning)
54+
- "UNRVL_ATTR": Presentation contains attributes with unrevealed values (warning)
55+
56+
## Presentation Pre-validation Errors
57+
58+
The following pre-verification checks are done, which will fail the proof (before calling anoncreds) and will result in the following message:
59+
60+
```
61+
VALUE_ERROR::<description of the failed validation>
62+
```
63+
64+
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.
65+
66+
A summary of the possible errors is:
67+
68+
- information missing in presentation exchange record
69+
- timestamp provided for irrevocable credential
70+
- referenced revocation registry not found on ledger
71+
- timestamp outside of reasonable range (future date or pre-dates revocation registry)
72+
- mis-match between provided and requested timestamps for non-revocation
73+
- mis-match between requested and provided attributes or predicates
74+
- self-attested attribute is provided for a requested attribute with restrictions
75+
- encoded value doesn't match raw value
76+
77+
## Anoncreds Verification Exceptions
78+
79+
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:
80+
81+
```
82+
VERIFY_ERROR::<the exception text>
83+
```
84+

aries_cloudagent/indy/credx/verifier.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from ...core.profile import Profile
99

10-
from ..verifier import IndyVerifier
10+
from ..verifier import IndyVerifier, PresVerifyMsg
1111

1212
LOGGER = logging.getLogger(__name__)
1313

@@ -33,7 +33,7 @@ async def verify_presentation(
3333
credential_definitions,
3434
rev_reg_defs,
3535
rev_reg_entries,
36-
) -> bool:
36+
) -> (bool, list):
3737
"""
3838
Verify a presentation.
3939
@@ -46,16 +46,21 @@ async def verify_presentation(
4646
rev_reg_entries: revocation registry entries
4747
"""
4848

49+
msgs = []
4950
try:
50-
self.non_revoc_intervals(pres_req, pres, credential_definitions)
51-
await self.check_timestamps(self.profile, pres_req, pres, rev_reg_defs)
52-
await self.pre_verify(pres_req, pres)
51+
msgs += self.non_revoc_intervals(pres_req, pres, credential_definitions)
52+
msgs += await self.check_timestamps(
53+
self.profile, pres_req, pres, rev_reg_defs
54+
)
55+
msgs += await self.pre_verify(pres_req, pres)
5356
except ValueError as err:
57+
s = str(err)
58+
msgs.append(f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{s}")
5459
LOGGER.error(
5560
f"Presentation on nonce={pres_req['nonce']} "
5661
f"cannot be validated: {str(err)}"
5762
)
58-
return False
63+
return (False, msgs)
5964

6065
try:
6166
presentation = Presentation.load(pres)
@@ -68,11 +73,13 @@ async def verify_presentation(
6873
rev_reg_defs.values(),
6974
rev_reg_entries,
7075
)
71-
except CredxError:
76+
except CredxError as err:
77+
s = str(err)
78+
msgs.append(f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{s}")
7279
LOGGER.exception(
7380
f"Validation of presentation on nonce={pres_req['nonce']} "
7481
"failed with error"
7582
)
7683
verified = False
7784

78-
return verified
85+
return (verified, msgs)

aries_cloudagent/indy/sdk/tests/test_verifier.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ async def test_verify_presentation(self, mock_verify):
336336
) as mock_get_ledger:
337337
mock_get_ledger.return_value = (None, self.ledger)
338338
INDY_PROOF_REQ_X = deepcopy(INDY_PROOF_REQ_PRED_NAMES)
339-
verified = await self.verifier.verify_presentation(
339+
(verified, msgs) = await self.verifier.verify_presentation(
340340
INDY_PROOF_REQ_X,
341341
INDY_PROOF_PRED_NAMES,
342342
"schemas",
@@ -370,7 +370,7 @@ async def test_verify_presentation_x_indy(self, mock_verify):
370370
IndyLedgerRequestsExecutor, "get_ledger_for_identifier"
371371
) as mock_get_ledger:
372372
mock_get_ledger.return_value = ("test", self.ledger)
373-
verified = await self.verifier.verify_presentation(
373+
(verified, msgs) = await self.verifier.verify_presentation(
374374
INDY_PROOF_REQ_NAME,
375375
INDY_PROOF_NAME,
376376
"schemas",
@@ -397,7 +397,7 @@ async def test_check_encoding_attr(self, mock_verify):
397397
) as mock_get_ledger:
398398
mock_get_ledger.return_value = (None, self.ledger)
399399
mock_verify.return_value = True
400-
verified = await self.verifier.verify_presentation(
400+
(verified, msgs) = await self.verifier.verify_presentation(
401401
INDY_PROOF_REQ_NAME,
402402
INDY_PROOF_NAME,
403403
"schemas",
@@ -415,6 +415,8 @@ async def test_check_encoding_attr(self, mock_verify):
415415
json.dumps("rev_reg_entries"),
416416
)
417417
assert verified is True
418+
assert len(msgs) == 1
419+
assert "TS_OUT_NRI::19_uuid" in msgs
418420

419421
@async_mock.patch("indy.anoncreds.verifier_verify_proof")
420422
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):
426428
IndyLedgerRequestsExecutor, "get_ledger_for_identifier"
427429
) as mock_get_ledger:
428430
mock_get_ledger.return_value = ("test", self.ledger)
429-
verified = await self.verifier.verify_presentation(
431+
(verified, msgs) = await self.verifier.verify_presentation(
430432
INDY_PROOF_REQ_NAME,
431433
INDY_PROOF_X,
432434
"schemas",
@@ -438,6 +440,11 @@ async def test_check_encoding_attr_tamper_raw(self, mock_verify):
438440
mock_verify.assert_not_called()
439441

440442
assert verified is False
443+
assert len(msgs) == 2
444+
assert "TS_OUT_NRI::19_uuid" in msgs
445+
assert (
446+
"VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs
447+
)
441448

442449
@async_mock.patch("indy.anoncreds.verifier_verify_proof")
443450
async def test_check_encoding_attr_tamper_encoded(self, mock_verify):
@@ -449,7 +456,7 @@ async def test_check_encoding_attr_tamper_encoded(self, mock_verify):
449456
IndyLedgerRequestsExecutor, "get_ledger_for_identifier"
450457
) as mock_get_ledger:
451458
mock_get_ledger.return_value = (None, self.ledger)
452-
verified = await self.verifier.verify_presentation(
459+
(verified, msgs) = await self.verifier.verify_presentation(
453460
INDY_PROOF_REQ_NAME,
454461
INDY_PROOF_X,
455462
"schemas",
@@ -461,6 +468,11 @@ async def test_check_encoding_attr_tamper_encoded(self, mock_verify):
461468
mock_verify.assert_not_called()
462469

463470
assert verified is False
471+
assert len(msgs) == 2
472+
assert "TS_OUT_NRI::19_uuid" in msgs
473+
assert (
474+
"VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs
475+
)
464476

465477
@async_mock.patch("indy.anoncreds.verifier_verify_proof")
466478
async def test_check_pred_names(self, mock_verify):
@@ -470,7 +482,7 @@ async def test_check_pred_names(self, mock_verify):
470482
mock_get_ledger.return_value = ("test", self.ledger)
471483
mock_verify.return_value = True
472484
INDY_PROOF_REQ_X = deepcopy(INDY_PROOF_REQ_PRED_NAMES)
473-
verified = await self.verifier.verify_presentation(
485+
(verified, msgs) = await self.verifier.verify_presentation(
474486
INDY_PROOF_REQ_X,
475487
INDY_PROOF_PRED_NAMES,
476488
"schemas",
@@ -491,6 +503,10 @@ async def test_check_pred_names(self, mock_verify):
491503
)
492504

493505
assert verified is True
506+
assert len(msgs) == 3
507+
assert "TS_OUT_NRI::18_uuid" in msgs
508+
assert "TS_OUT_NRI::18_id_GE_uuid" in msgs
509+
assert "TS_OUT_NRI::18_busid_GE_uuid" in msgs
494510

495511
@async_mock.patch("indy.anoncreds.verifier_verify_proof")
496512
async def test_check_pred_names_tamper_pred_value(self, mock_verify):
@@ -502,7 +518,7 @@ async def test_check_pred_names_tamper_pred_value(self, mock_verify):
502518
IndyLedgerRequestsExecutor, "get_ledger_for_identifier"
503519
) as mock_get_ledger:
504520
mock_get_ledger.return_value = (None, self.ledger)
505-
verified = await self.verifier.verify_presentation(
521+
(verified, msgs) = await self.verifier.verify_presentation(
506522
deepcopy(INDY_PROOF_REQ_PRED_NAMES),
507523
INDY_PROOF_X,
508524
"schemas",
@@ -514,6 +530,14 @@ async def test_check_pred_names_tamper_pred_value(self, mock_verify):
514530
mock_verify.assert_not_called()
515531

516532
assert verified is False
533+
assert len(msgs) == 4
534+
assert "RMV_RFNT_NRI::18_uuid" in msgs
535+
assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs
536+
assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs
537+
assert (
538+
"VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid"
539+
in msgs
540+
)
517541

518542
@async_mock.patch("indy.anoncreds.verifier_verify_proof")
519543
async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify):
@@ -523,7 +547,7 @@ async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify):
523547
IndyLedgerRequestsExecutor, "get_ledger_for_identifier"
524548
) as mock_get_ledger:
525549
mock_get_ledger.return_value = (None, self.ledger)
526-
verified = await self.verifier.verify_presentation(
550+
(verified, msgs) = await self.verifier.verify_presentation(
527551
INDY_PROOF_REQ_X,
528552
INDY_PROOF_PRED_NAMES,
529553
"schemas",
@@ -535,6 +559,14 @@ async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify):
535559
mock_verify.assert_not_called()
536560

537561
assert verified is False
562+
assert len(msgs) == 4
563+
assert "RMV_RFNT_NRI::18_uuid" in msgs
564+
assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs
565+
assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs
566+
assert (
567+
"VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid"
568+
in msgs
569+
)
538570

539571
@async_mock.patch("indy.anoncreds.verifier_verify_proof")
540572
async def test_check_pred_names_tamper_attr_groups(self, mock_verify):
@@ -546,7 +578,7 @@ async def test_check_pred_names_tamper_attr_groups(self, mock_verify):
546578
IndyLedgerRequestsExecutor, "get_ledger_for_identifier"
547579
) as mock_get_ledger:
548580
mock_get_ledger.return_value = ("test", self.ledger)
549-
verified = await self.verifier.verify_presentation(
581+
(verified, msgs) = await self.verifier.verify_presentation(
550582
deepcopy(INDY_PROOF_REQ_PRED_NAMES),
551583
INDY_PROOF_X,
552584
"schemas",
@@ -558,3 +590,7 @@ async def test_check_pred_names_tamper_attr_groups(self, mock_verify):
558590
mock_verify.assert_not_called()
559591

560592
assert verified is False
593+
assert len(msgs) == 3
594+
assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs
595+
assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs
596+
assert "VALUE_ERROR::Missing requested attribute group 18_uuid" in msgs

aries_cloudagent/indy/sdk/verifier.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from ...core.profile import Profile
1010

11-
from ..verifier import IndyVerifier
11+
from ..verifier import IndyVerifier, PresVerifyMsg
1212

1313
LOGGER = logging.getLogger(__name__)
1414

@@ -34,7 +34,7 @@ async def verify_presentation(
3434
credential_definitions,
3535
rev_reg_defs,
3636
rev_reg_entries,
37-
) -> bool:
37+
) -> (bool, list):
3838
"""
3939
Verify a presentation.
4040
@@ -49,16 +49,21 @@ async def verify_presentation(
4949

5050
LOGGER.debug(f">>> received presentation: {pres}")
5151
LOGGER.debug(f">>> for pres_req: {pres_req}")
52+
msgs = []
5253
try:
53-
self.non_revoc_intervals(pres_req, pres, credential_definitions)
54-
await self.check_timestamps(self.profile, pres_req, pres, rev_reg_defs)
55-
await self.pre_verify(pres_req, pres)
54+
msgs += self.non_revoc_intervals(pres_req, pres, credential_definitions)
55+
msgs += await self.check_timestamps(
56+
self.profile, pres_req, pres, rev_reg_defs
57+
)
58+
msgs += await self.pre_verify(pres_req, pres)
5659
except ValueError as err:
60+
s = str(err)
61+
msgs.append(f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{s}")
5762
LOGGER.error(
5863
f"Presentation on nonce={pres_req['nonce']} "
5964
f"cannot be validated: {str(err)}"
6065
)
61-
return False
66+
return (False, msgs)
6267

6368
LOGGER.debug(f">>> verifying presentation: {pres}")
6469
LOGGER.debug(f">>> for pres_req: {pres_req}")
@@ -71,11 +76,13 @@ async def verify_presentation(
7176
json.dumps(rev_reg_defs),
7277
json.dumps(rev_reg_entries),
7378
)
74-
except IndyError:
79+
except IndyError as err:
80+
s = str(err)
81+
msgs.append(f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{s}")
7582
LOGGER.exception(
7683
f"Validation of presentation on nonce={pres_req['nonce']} "
7784
"failed with error"
7885
)
7986
verified = False
8087

81-
return verified
88+
return (verified, msgs)

0 commit comments

Comments
 (0)