Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: enabled handling VPs (request, creation, verification) with different VCs #1956

Merged
merged 17 commits into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Multicredentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Multi-Credentials

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.

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.

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.

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.
7 changes: 4 additions & 3 deletions aries_cloudagent/messaging/decorators/attach_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from ..valid import (
BASE64,
BASE64URL_NO_PAD,
DictOrDictListField,
INDY_ISO8601_DATETIME,
JWS_HEADER_KID,
SHA256,
Expand Down Expand Up @@ -228,7 +229,7 @@ def __init__(
sha256_: str = None,
links_: Union[Sequence[str], str] = None,
base64_: str = None,
json_: dict = None,
json_: Union[Sequence[dict], dict] = None,
):
"""
Initialize decorator data.
Expand Down Expand Up @@ -488,7 +489,7 @@ def validate_data_spec(self, data: Mapping, **kwargs):
required=False,
data_key="jws",
)
json_ = fields.Dict(
json_ = DictOrDictListField(
description="JSON-serialized data",
required=False,
example='{"sample": "content"}',
Expand Down Expand Up @@ -615,7 +616,7 @@ def data_base64(
@classmethod
def data_json(
cls,
mapping: dict,
mapping: Union[Sequence[dict], dict],
*,
ident: str = None,
description: str = None,
Expand Down
157 changes: 97 additions & 60 deletions aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,7 @@ async def create_vp(
challenge: str = None,
domain: str = None,
records_filter: dict = None,
) -> dict:
) -> Union[Sequence[dict], dict]:
"""
Create VerifiablePresentation.

Expand All @@ -1244,78 +1244,99 @@ async def create_vp(
req = await self.make_requirement(
srs=pd.submission_requirements, descriptors=pd.input_descriptors
)
result = await self.apply_requirements(
req=req, credentials=credentials, records_filter=records_filter
)
applicable_creds, descriptor_maps = await self.merge(result)
applicable_creds_list = []
for credential in applicable_creds:
applicable_creds_list.append(credential.cred_value)
if (
not self.profile.settings.get("debug.auto_respond_presentation_request")
and not records_filter
and len(applicable_creds_list) > 1
):
raise DIFPresExchError(
"Multiple credentials are applicable for presentation_definition "
f"{pd.id} and --auto-respond-presentation-request setting is not "
"enabled. Please specify which credentials should be applied to "
"which input_descriptors using record_ids filter."
result = []
if req.nested_req:
for nested_req in req.nested_req:
res = await self.apply_requirements(
req=nested_req,
credentials=credentials,
records_filter=records_filter,
)
result.append(res)
else:
res = await self.apply_requirements(
req=req, credentials=credentials, records_filter=records_filter
)
# submission_property
submission_property = PresentationSubmission(
id=str(uuid4()), definition_id=pd.id, descriptor_maps=descriptor_maps
)
if self.is_holder:
(
issuer_id,
filtered_creds_list,
) = await self.get_sign_key_credential_subject_id(
applicable_creds=applicable_creds
result.append(res)

result_vp = []
for res in result:
applicable_creds, descriptor_maps = await self.merge(res)
applicable_creds_list = []
for credential in applicable_creds:
applicable_creds_list.append(credential.cred_value)
if (
not self.profile.settings.get("debug.auto_respond_presentation_request")
and not records_filter
and len(applicable_creds_list) > 1
):
raise DIFPresExchError(
"Multiple credentials are applicable for presentation_definition "
f"{pd.id} and --auto-respond-presentation-request setting is not "
"enabled. Please specify which credentials should be applied to "
"which input_descriptors using record_ids filter."
)
# submission_property
submission_property = PresentationSubmission(
id=str(uuid4()), definition_id=pd.id, descriptor_maps=descriptor_maps
)
if not issuer_id and len(filtered_creds_list) == 0:
vp = await create_presentation(credentials=applicable_creds_list)
vp["presentation_submission"] = submission_property.serialize()
if self.proof_type is BbsBlsSignature2020.signature_type:
vp["@context"].append(SECURITY_CONTEXT_BBS_URL)
return vp
else:
vp = await create_presentation(credentials=filtered_creds_list)
else:
if not self.pres_signing_did:
if self.is_holder:
(
issuer_id,
filtered_creds_list,
) = await self.get_sign_key_credential_subject_id(
applicable_creds=applicable_creds
)
if not issuer_id:
if not issuer_id and len(filtered_creds_list) == 0:
vp = await create_presentation(credentials=applicable_creds_list)
vp["presentation_submission"] = submission_property.serialize()
if self.proof_type is BbsBlsSignature2020.signature_type:
vp["@context"].append(SECURITY_CONTEXT_BBS_URL)
return vp
result_vp.append(vp)
continue
else:
vp = await create_presentation(credentials=filtered_creds_list)
else:
issuer_id = self.pres_signing_did
vp = await create_presentation(credentials=applicable_creds_list)
vp["presentation_submission"] = submission_property.serialize()
if self.proof_type is BbsBlsSignature2020.signature_type:
vp["@context"].append(SECURITY_CONTEXT_BBS_URL)
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
issue_suite = await self._get_issue_suite(
wallet=wallet,
issuer_id=issuer_id,
)
signed_vp = await sign_presentation(
presentation=vp,
suite=issue_suite,
challenge=challenge,
document_loader=document_loader,
)
return signed_vp
if not self.pres_signing_did:
(
issuer_id,
filtered_creds_list,
) = await self.get_sign_key_credential_subject_id(
applicable_creds=applicable_creds
)
if not issuer_id:
vp = await create_presentation(
credentials=applicable_creds_list
)
vp["presentation_submission"] = submission_property.serialize()
if self.proof_type is BbsBlsSignature2020.signature_type:
vp["@context"].append(SECURITY_CONTEXT_BBS_URL)
result_vp.append(vp)
continue
else:
vp = await create_presentation(credentials=filtered_creds_list)
else:
issuer_id = self.pres_signing_did
vp = await create_presentation(credentials=applicable_creds_list)
vp["presentation_submission"] = submission_property.serialize()
if self.proof_type is BbsBlsSignature2020.signature_type:
vp["@context"].append(SECURITY_CONTEXT_BBS_URL)
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
issue_suite = await self._get_issue_suite(
wallet=wallet,
issuer_id=issuer_id,
)
signed_vp = await sign_presentation(
presentation=vp,
suite=issue_suite,
challenge=challenge,
document_loader=document_loader,
)
result_vp.append(signed_vp)
if len(result_vp) == 1:
return result_vp[0]
return result_vp

def check_if_cred_id_derived(self, id: str) -> bool:
"""Check if credential or credentialSubjet id is derived."""
Expand Down Expand Up @@ -1367,7 +1388,7 @@ async def merge(
async def verify_received_pres(
self,
pd: PresentationDefinition,
pres: dict,
pres: Union[Sequence[dict], dict],
):
"""
Verify credentials received in presentation.
Expand All @@ -1376,8 +1397,24 @@ async def verify_received_pres(
pres: received VerifiablePresentation
pd: PresentationDefinition
"""
descriptor_map_list = pres["presentation_submission"].get("descriptor_map")
input_descriptors = pd.input_descriptors
if isinstance(pres, Sequence):
for pr in pres:
descriptor_map_list = pr["presentation_submission"].get(
"descriptor_map"
)
await self.__verify_desc_map_list(
descriptor_map_list, pr, input_descriptors
)
else:
descriptor_map_list = pres["presentation_submission"].get("descriptor_map")
await self.__verify_desc_map_list(
descriptor_map_list, pres, input_descriptors
)

async def __verify_desc_map_list(
self, descriptor_map_list, pres, input_descriptors
):
inp_desc_id_contraint_map = {}
inp_desc_id_schema_one_of_filter = set()
inp_desc_id_schemas_map = {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
from copy import deepcopy
from datetime import datetime
from typing import Sequence
from uuid import uuid4

import mock as async_mock
Expand Down Expand Up @@ -105,7 +106,17 @@ async def test_load_cred_json_a(self, setup_tuple, profile):
pd=tmp_pd[0],
challenge="1f44d55f-f161-4938-a659-f8026467f126",
)
assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1]

if isinstance(tmp_vp, Sequence):
cred_count_list = []
for tmp_vp_single in tmp_vp:
cred_count_list.append(
len(tmp_vp_single.get("verifiableCredential"))
)

assert min(cred_count_list) == tmp_pd[1]
else:
assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1]

@pytest.mark.asyncio
@pytest.mark.ursa_bbs_signatures
Expand All @@ -122,7 +133,17 @@ async def test_load_cred_json_b(self, setup_tuple, profile):
pd=tmp_pd[0],
challenge="1f44d55f-f161-4938-a659-f8026467f126",
)
assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1]

if isinstance(tmp_vp, Sequence):
cred_count_list = []
for tmp_vp_single in tmp_vp:
cred_count_list.append(
len(tmp_vp_single.get("verifiableCredential"))
)

assert min(cred_count_list) == tmp_pd[1]
else:
assert len(tmp_vp.get("verifiableCredential")) == tmp_pd[1]

@pytest.mark.asyncio
async def test_to_requirement_catch_errors(self, profile):
Expand Down Expand Up @@ -2266,11 +2287,12 @@ async def test_create_vp_no_issuer(self, profile, setup_tuple):
pd=pd_list[0][0],
challenge="3fa85f64-5717-4562-b3fc-2c963f66afa7",
)
assert vp["test"] == "1"
assert (
vp["presentation_submission"]["definition_id"]
== "32f54163-7166-48f1-93d8-ff217bdb0653"
)
for vp_single in vp:
assert vp_single["test"] == "1"
assert (
vp_single["presentation_submission"]["definition_id"]
== "32f54163-7166-48f1-93d8-ff217bdb0653"
)

@pytest.mark.asyncio
@pytest.mark.ursa_bbs_signatures
Expand Down Expand Up @@ -2326,8 +2348,9 @@ async def test_create_vp_with_bbs_suite(self, profile, setup_tuple):
pd=pd_list[0][0],
challenge="3fa85f64-5717-4562-b3fc-2c963f66afa7",
)
assert vp["test"] == "1"
assert SECURITY_CONTEXT_BBS_URL in vp["@context"]
for vp_single in vp:
assert vp_single["test"] == "1"
assert SECURITY_CONTEXT_BBS_URL in vp_single["@context"]

@pytest.mark.asyncio
@pytest.mark.ursa_bbs_signatures
Expand Down Expand Up @@ -2380,8 +2403,10 @@ async def test_create_vp_no_issuer_with_bbs_suite(self, profile, setup_tuple):
pd=pd_list[0][0],
challenge="3fa85f64-5717-4562-b3fc-2c963f66afa7",
)
assert vp["test"] == "1"
assert SECURITY_CONTEXT_BBS_URL in vp["@context"]
# 2 sub_reqs, vp is a sequence
for vp_single in vp:
assert vp_single["test"] == "1"
assert SECURITY_CONTEXT_BBS_URL in vp_single["@context"]

@pytest.mark.asyncio
@pytest.mark.ursa_bbs_signatures
Expand Down Expand Up @@ -3057,6 +3082,8 @@ async def test_multiple_applicable_creds_with_no_id(self, profile, setup_tuple):
pd=tmp_pd[0],
challenge="1f44d55f-f161-4938-a659-f8026467f126",
)
# only 1 sub_req
assert isinstance(tmp_vp, dict)
assert len(tmp_vp["verifiableCredential"]) == 2
assert (
tmp_vp.get("verifiableCredential")[0]
Expand All @@ -3077,19 +3104,22 @@ async def test_multiple_applicable_creds_with_no_id(self, profile, setup_tuple):
pd=tmp_pd[0],
challenge="1f44d55f-f161-4938-a659-f8026467f126",
)
assert len(tmp_vp["verifiableCredential"]) == 2
assert (
tmp_vp.get("verifiableCredential")[0]
.get("credentialSubject")
.get("givenName")
== "TEST"
)
assert (
tmp_vp.get("verifiableCredential")[1]
.get("credentialSubject")
.get("givenName")
== "TEST"
)
assert isinstance(tmp_vp, Sequence)
# 1 for each submission requirement group
assert len(tmp_vp) == 3
for tmp_vp_single in tmp_vp:
assert (
tmp_vp_single.get("verifiableCredential")[0]
.get("credentialSubject")
.get("givenName")
== "TEST"
)
assert (
tmp_vp_single.get("verifiableCredential")[1]
.get("credentialSubject")
.get("givenName")
== "TEST"
)

@pytest.mark.asyncio
@pytest.mark.ursa_bbs_signatures
Expand Down
Loading