Skip to content

Commit 043d8c1

Browse files
authored
Merge pull request openwallet-foundation#1956 from teanas/feature/enable-aggr-vcs
Feature: enabled handling VPs (request, creation, verification) with different VCs
2 parents 6629d7d + de84288 commit 043d8c1

13 files changed

Lines changed: 773 additions & 121 deletions

File tree

Multicredentials.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Multi-Credentials
2+
3+
It is a known fact that multiple AnonCreds can be combined to present a presentation proof with an "and" logical operator: For instance, a verifier can ask for the "name" claim from an eID and the "address" claim from a bank statement to have a single proof that is either valid or invalid. With the Present Proof Protocol v2, it is possible to have "and" and "or" logical operators for AnonCreds and/or W3C Verifiable Credentials.
4+
5+
With the Present Proof Protocol v2, verifiers can ask for a combination of credentials as proof. For instance, a Verifier can ask a claim from an AnonCreds **and** a verifiable presentation from a W3C Verifiable Credential, which would open the possibilities of Aries Cloud Agent Python being used for rather complex presentation proof requests that wouldn't be possible without the support of AnonCreds or W3C Verifiable Credentials.
6+
7+
Moreover, it is possible to make similar presentation proof requests using the or logical operator. For instance, a verifier can ask for either an eID in AnonCreds format or an eID in W3C Verifiable Credential format. This has the potential to solve the interoperability problem of different credential formats and ecosystems from a user point of view by shifting the requirement of holding/accepting different credential formats from identity holders to verifiers. Here again, using Aries Cloud Agent Python as the underlying verifier agent can tackle such complex presentation proof requests since the agent is capable of verifying both type of credential formats and proof types.
8+
9+
In the future, it would be even possible to put mDoc as an attachment with an and or or logical operation, along with AnonCreds and/or W3C Verifiable Credentials. For this to happen, Aca-Py either needs the capabilities to validate mDocs internally or to connect third-party endpoints to validate and get a response.

aries_cloudagent/messaging/decorators/attach_decorator.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ..valid import (
3131
BASE64,
3232
BASE64URL_NO_PAD,
33+
DictOrDictListField,
3334
INDY_ISO8601_DATETIME,
3435
JWS_HEADER_KID,
3536
SHA256,
@@ -228,7 +229,7 @@ def __init__(
228229
sha256_: str = None,
229230
links_: Union[Sequence[str], str] = None,
230231
base64_: str = None,
231-
json_: dict = None,
232+
json_: Union[Sequence[dict], dict] = None,
232233
):
233234
"""
234235
Initialize decorator data.
@@ -488,7 +489,7 @@ def validate_data_spec(self, data: Mapping, **kwargs):
488489
required=False,
489490
data_key="jws",
490491
)
491-
json_ = fields.Dict(
492+
json_ = DictOrDictListField(
492493
description="JSON-serialized data",
493494
required=False,
494495
example='{"sample": "content"}',
@@ -615,7 +616,7 @@ def data_base64(
615616
@classmethod
616617
def data_json(
617618
cls,
618-
mapping: dict,
619+
mapping: Union[Sequence[dict], dict],
619620
*,
620621
ident: str = None,
621622
description: str = None,

aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py

Lines changed: 97 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,7 @@ async def create_vp(
12301230
challenge: str = None,
12311231
domain: str = None,
12321232
records_filter: dict = None,
1233-
) -> dict:
1233+
) -> Union[Sequence[dict], dict]:
12341234
"""
12351235
Create VerifiablePresentation.
12361236
@@ -1244,78 +1244,99 @@ async def create_vp(
12441244
req = await self.make_requirement(
12451245
srs=pd.submission_requirements, descriptors=pd.input_descriptors
12461246
)
1247-
result = await self.apply_requirements(
1248-
req=req, credentials=credentials, records_filter=records_filter
1249-
)
1250-
applicable_creds, descriptor_maps = await self.merge(result)
1251-
applicable_creds_list = []
1252-
for credential in applicable_creds:
1253-
applicable_creds_list.append(credential.cred_value)
1254-
if (
1255-
not self.profile.settings.get("debug.auto_respond_presentation_request")
1256-
and not records_filter
1257-
and len(applicable_creds_list) > 1
1258-
):
1259-
raise DIFPresExchError(
1260-
"Multiple credentials are applicable for presentation_definition "
1261-
f"{pd.id} and --auto-respond-presentation-request setting is not "
1262-
"enabled. Please specify which credentials should be applied to "
1263-
"which input_descriptors using record_ids filter."
1247+
result = []
1248+
if req.nested_req:
1249+
for nested_req in req.nested_req:
1250+
res = await self.apply_requirements(
1251+
req=nested_req,
1252+
credentials=credentials,
1253+
records_filter=records_filter,
1254+
)
1255+
result.append(res)
1256+
else:
1257+
res = await self.apply_requirements(
1258+
req=req, credentials=credentials, records_filter=records_filter
12641259
)
1265-
# submission_property
1266-
submission_property = PresentationSubmission(
1267-
id=str(uuid4()), definition_id=pd.id, descriptor_maps=descriptor_maps
1268-
)
1269-
if self.is_holder:
1270-
(
1271-
issuer_id,
1272-
filtered_creds_list,
1273-
) = await self.get_sign_key_credential_subject_id(
1274-
applicable_creds=applicable_creds
1260+
result.append(res)
1261+
1262+
result_vp = []
1263+
for res in result:
1264+
applicable_creds, descriptor_maps = await self.merge(res)
1265+
applicable_creds_list = []
1266+
for credential in applicable_creds:
1267+
applicable_creds_list.append(credential.cred_value)
1268+
if (
1269+
not self.profile.settings.get("debug.auto_respond_presentation_request")
1270+
and not records_filter
1271+
and len(applicable_creds_list) > 1
1272+
):
1273+
raise DIFPresExchError(
1274+
"Multiple credentials are applicable for presentation_definition "
1275+
f"{pd.id} and --auto-respond-presentation-request setting is not "
1276+
"enabled. Please specify which credentials should be applied to "
1277+
"which input_descriptors using record_ids filter."
1278+
)
1279+
# submission_property
1280+
submission_property = PresentationSubmission(
1281+
id=str(uuid4()), definition_id=pd.id, descriptor_maps=descriptor_maps
12751282
)
1276-
if not issuer_id and len(filtered_creds_list) == 0:
1277-
vp = await create_presentation(credentials=applicable_creds_list)
1278-
vp["presentation_submission"] = submission_property.serialize()
1279-
if self.proof_type is BbsBlsSignature2020.signature_type:
1280-
vp["@context"].append(SECURITY_CONTEXT_BBS_URL)
1281-
return vp
1282-
else:
1283-
vp = await create_presentation(credentials=filtered_creds_list)
1284-
else:
1285-
if not self.pres_signing_did:
1283+
if self.is_holder:
12861284
(
12871285
issuer_id,
12881286
filtered_creds_list,
12891287
) = await self.get_sign_key_credential_subject_id(
12901288
applicable_creds=applicable_creds
12911289
)
1292-
if not issuer_id:
1290+
if not issuer_id and len(filtered_creds_list) == 0:
12931291
vp = await create_presentation(credentials=applicable_creds_list)
12941292
vp["presentation_submission"] = submission_property.serialize()
12951293
if self.proof_type is BbsBlsSignature2020.signature_type:
12961294
vp["@context"].append(SECURITY_CONTEXT_BBS_URL)
1297-
return vp
1295+
result_vp.append(vp)
1296+
continue
12981297
else:
12991298
vp = await create_presentation(credentials=filtered_creds_list)
13001299
else:
1301-
issuer_id = self.pres_signing_did
1302-
vp = await create_presentation(credentials=applicable_creds_list)
1303-
vp["presentation_submission"] = submission_property.serialize()
1304-
if self.proof_type is BbsBlsSignature2020.signature_type:
1305-
vp["@context"].append(SECURITY_CONTEXT_BBS_URL)
1306-
async with self.profile.session() as session:
1307-
wallet = session.inject(BaseWallet)
1308-
issue_suite = await self._get_issue_suite(
1309-
wallet=wallet,
1310-
issuer_id=issuer_id,
1311-
)
1312-
signed_vp = await sign_presentation(
1313-
presentation=vp,
1314-
suite=issue_suite,
1315-
challenge=challenge,
1316-
document_loader=document_loader,
1317-
)
1318-
return signed_vp
1300+
if not self.pres_signing_did:
1301+
(
1302+
issuer_id,
1303+
filtered_creds_list,
1304+
) = await self.get_sign_key_credential_subject_id(
1305+
applicable_creds=applicable_creds
1306+
)
1307+
if not issuer_id:
1308+
vp = await create_presentation(
1309+
credentials=applicable_creds_list
1310+
)
1311+
vp["presentation_submission"] = submission_property.serialize()
1312+
if self.proof_type is BbsBlsSignature2020.signature_type:
1313+
vp["@context"].append(SECURITY_CONTEXT_BBS_URL)
1314+
result_vp.append(vp)
1315+
continue
1316+
else:
1317+
vp = await create_presentation(credentials=filtered_creds_list)
1318+
else:
1319+
issuer_id = self.pres_signing_did
1320+
vp = await create_presentation(credentials=applicable_creds_list)
1321+
vp["presentation_submission"] = submission_property.serialize()
1322+
if self.proof_type is BbsBlsSignature2020.signature_type:
1323+
vp["@context"].append(SECURITY_CONTEXT_BBS_URL)
1324+
async with self.profile.session() as session:
1325+
wallet = session.inject(BaseWallet)
1326+
issue_suite = await self._get_issue_suite(
1327+
wallet=wallet,
1328+
issuer_id=issuer_id,
1329+
)
1330+
signed_vp = await sign_presentation(
1331+
presentation=vp,
1332+
suite=issue_suite,
1333+
challenge=challenge,
1334+
document_loader=document_loader,
1335+
)
1336+
result_vp.append(signed_vp)
1337+
if len(result_vp) == 1:
1338+
return result_vp[0]
1339+
return result_vp
13191340

13201341
def check_if_cred_id_derived(self, id: str) -> bool:
13211342
"""Check if credential or credentialSubjet id is derived."""
@@ -1367,7 +1388,7 @@ async def merge(
13671388
async def verify_received_pres(
13681389
self,
13691390
pd: PresentationDefinition,
1370-
pres: dict,
1391+
pres: Union[Sequence[dict], dict],
13711392
):
13721393
"""
13731394
Verify credentials received in presentation.
@@ -1376,8 +1397,24 @@ async def verify_received_pres(
13761397
pres: received VerifiablePresentation
13771398
pd: PresentationDefinition
13781399
"""
1379-
descriptor_map_list = pres["presentation_submission"].get("descriptor_map")
13801400
input_descriptors = pd.input_descriptors
1401+
if isinstance(pres, Sequence):
1402+
for pr in pres:
1403+
descriptor_map_list = pr["presentation_submission"].get(
1404+
"descriptor_map"
1405+
)
1406+
await self.__verify_desc_map_list(
1407+
descriptor_map_list, pr, input_descriptors
1408+
)
1409+
else:
1410+
descriptor_map_list = pres["presentation_submission"].get("descriptor_map")
1411+
await self.__verify_desc_map_list(
1412+
descriptor_map_list, pres, input_descriptors
1413+
)
1414+
1415+
async def __verify_desc_map_list(
1416+
self, descriptor_map_list, pres, input_descriptors
1417+
):
13811418
inp_desc_id_contraint_map = {}
13821419
inp_desc_id_schema_one_of_filter = set()
13831420
inp_desc_id_schemas_map = {}

aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
from copy import deepcopy
33
from datetime import datetime
4+
from typing import Sequence
45
from uuid import uuid4
56

67
import mock as async_mock
@@ -105,7 +106,17 @@ async def test_load_cred_json_a(self, setup_tuple, profile):
105106
pd=tmp_pd[0],
106107
challenge="1f44d55f-f161-4938-a659-f8026467f126",
107108
)
108-
assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1]
109+
110+
if isinstance(tmp_vp, Sequence):
111+
cred_count_list = []
112+
for tmp_vp_single in tmp_vp:
113+
cred_count_list.append(
114+
len(tmp_vp_single.get("verifiableCredential"))
115+
)
116+
117+
assert min(cred_count_list) == tmp_pd[1]
118+
else:
119+
assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1]
109120

110121
@pytest.mark.asyncio
111122
@pytest.mark.ursa_bbs_signatures
@@ -122,7 +133,17 @@ async def test_load_cred_json_b(self, setup_tuple, profile):
122133
pd=tmp_pd[0],
123134
challenge="1f44d55f-f161-4938-a659-f8026467f126",
124135
)
125-
assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1]
136+
137+
if isinstance(tmp_vp, Sequence):
138+
cred_count_list = []
139+
for tmp_vp_single in tmp_vp:
140+
cred_count_list.append(
141+
len(tmp_vp_single.get("verifiableCredential"))
142+
)
143+
144+
assert min(cred_count_list) == tmp_pd[1]
145+
else:
146+
assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1]
126147

127148
@pytest.mark.asyncio
128149
async def test_to_requirement_catch_errors(self, profile):
@@ -2266,11 +2287,12 @@ async def test_create_vp_no_issuer(self, profile, setup_tuple):
22662287
pd=pd_list[0][0],
22672288
challenge="3fa85f64-5717-4562-b3fc-2c963f66afa7",
22682289
)
2269-
assert vp["test"] == "1"
2270-
assert (
2271-
vp["presentation_submission"]["definition_id"]
2272-
== "32f54163-7166-48f1-93d8-ff217bdb0653"
2273-
)
2290+
for vp_single in vp:
2291+
assert vp_single["test"] == "1"
2292+
assert (
2293+
vp_single["presentation_submission"]["definition_id"]
2294+
== "32f54163-7166-48f1-93d8-ff217bdb0653"
2295+
)
22742296

22752297
@pytest.mark.asyncio
22762298
@pytest.mark.ursa_bbs_signatures
@@ -2326,8 +2348,9 @@ async def test_create_vp_with_bbs_suite(self, profile, setup_tuple):
23262348
pd=pd_list[0][0],
23272349
challenge="3fa85f64-5717-4562-b3fc-2c963f66afa7",
23282350
)
2329-
assert vp["test"] == "1"
2330-
assert SECURITY_CONTEXT_BBS_URL in vp["@context"]
2351+
for vp_single in vp:
2352+
assert vp_single["test"] == "1"
2353+
assert SECURITY_CONTEXT_BBS_URL in vp_single["@context"]
23312354

23322355
@pytest.mark.asyncio
23332356
@pytest.mark.ursa_bbs_signatures
@@ -2380,8 +2403,10 @@ async def test_create_vp_no_issuer_with_bbs_suite(self, profile, setup_tuple):
23802403
pd=pd_list[0][0],
23812404
challenge="3fa85f64-5717-4562-b3fc-2c963f66afa7",
23822405
)
2383-
assert vp["test"] == "1"
2384-
assert SECURITY_CONTEXT_BBS_URL in vp["@context"]
2406+
# 2 sub_reqs, vp is a sequence
2407+
for vp_single in vp:
2408+
assert vp_single["test"] == "1"
2409+
assert SECURITY_CONTEXT_BBS_URL in vp_single["@context"]
23852410

23862411
@pytest.mark.asyncio
23872412
@pytest.mark.ursa_bbs_signatures
@@ -3057,6 +3082,8 @@ async def test_multiple_applicable_creds_with_no_id(self, profile, setup_tuple):
30573082
pd=tmp_pd[0],
30583083
challenge="1f44d55f-f161-4938-a659-f8026467f126",
30593084
)
3085+
# only 1 sub_req
3086+
assert isinstance(tmp_vp, dict)
30603087
assert len(tmp_vp["verifiableCredential"]) == 2
30613088
assert (
30623089
tmp_vp.get("verifiableCredential")[0]
@@ -3077,19 +3104,22 @@ async def test_multiple_applicable_creds_with_no_id(self, profile, setup_tuple):
30773104
pd=tmp_pd[0],
30783105
challenge="1f44d55f-f161-4938-a659-f8026467f126",
30793106
)
3080-
assert len(tmp_vp["verifiableCredential"]) == 2
3081-
assert (
3082-
tmp_vp.get("verifiableCredential")[0]
3083-
.get("credentialSubject")
3084-
.get("givenName")
3085-
== "TEST"
3086-
)
3087-
assert (
3088-
tmp_vp.get("verifiableCredential")[1]
3089-
.get("credentialSubject")
3090-
.get("givenName")
3091-
== "TEST"
3092-
)
3107+
assert isinstance(tmp_vp, Sequence)
3108+
# 1 for each submission requirement group
3109+
assert len(tmp_vp) == 3
3110+
for tmp_vp_single in tmp_vp:
3111+
assert (
3112+
tmp_vp_single.get("verifiableCredential")[0]
3113+
.get("credentialSubject")
3114+
.get("givenName")
3115+
== "TEST"
3116+
)
3117+
assert (
3118+
tmp_vp_single.get("verifiableCredential")[1]
3119+
.get("credentialSubject")
3120+
.get("givenName")
3121+
== "TEST"
3122+
)
30933123

30943124
@pytest.mark.asyncio
30953125
@pytest.mark.ursa_bbs_signatures

0 commit comments

Comments
 (0)