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

fix: ensure request matches offer, if sent #2341

Merged
merged 1 commit into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from ...models.cred_ex_record import V20CredExRecord
from ...models.detail.ld_proof import V20CredExRecordLDProof
from ..handler import CredFormatAttachment, V20CredFormatError, V20CredFormatHandler
from .models.cred_detail_options import LDProofVCDetailOptions
from .models.cred_detail import LDProofVCDetail, LDProofVCDetailSchema

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -361,6 +362,8 @@ async def _prepare_detail(
self, detail: LDProofVCDetail, holder_did: str = None
) -> LDProofVCDetail:
# Add BBS context if not present yet
assert detail.options and isinstance(detail.options, LDProofVCDetailOptions)
assert detail.credential and isinstance(detail.credential, VerifiableCredential)
if (
detail.options.proof_type == BbsBlsSignature2020.signature_type
and SECURITY_CONTEXT_BBS_URL not in detail.credential.context_urls
Expand All @@ -373,9 +376,21 @@ async def _prepare_detail(
):
detail.credential.add_context(SECURITY_CONTEXT_ED25519_2020_URL)

# add holder_did as credentialSubject.id (if provided)
if holder_did and holder_did.startswith("did:key"):
detail.credential.credential_subject["id"] = holder_did
# Permit late binding of credential subject:
# IFF credential subject doesn't already have an id, add holder_did as
# credentialSubject.id (if provided)
subject = detail.credential.credential_subject

# TODO if credential subject is a list, we're only binding the first...
# How should this be handled?
if isinstance(subject, list):
subject = subject[0]

if not subject:
raise V20CredFormatError("Credential subject is required")

if holder_did and holder_did.startswith("did:key") and "id" not in subject:
subject["id"] = holder_did

return detail

Expand Down Expand Up @@ -462,6 +477,34 @@ async def receive_request(
self, cred_ex_record: V20CredExRecord, cred_request_message: V20CredRequest
) -> None:
"""Receive linked data proof request."""
# Check that request hasn't substantially changed from offer, if offer sent
if cred_ex_record.cred_offer:
offer_detail_dict = cred_ex_record.cred_offer.attachment(
LDProofCredFormatHandler.format
)
req_detail_dict = cred_request_message.attachment(
LDProofCredFormatHandler.format
)

# If credentialSubject.id in offer, it should be the same in request
offer_id = (
offer_detail_dict["credential"].get("credentialSubject", {}).get("id")
)
request_id = (
req_detail_dict["credential"].get("credentialSubject", {}).get("id")
)
if offer_id and offer_id != request_id:
raise V20CredFormatError(
"Request credentialSubject.id must match offer credentialSubject.id"
)

# Nothing else should be different about the request
if request_id:
offer_detail_dict["credential"].setdefault("credentialSubject", {})[
"id"
] = request_id
if offer_detail_dict != req_detail_dict:
raise V20CredFormatError("Request must match offer if offer is sent")

async def issue_credential(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -623,13 +623,152 @@ async def test_create_request_x_no_data(self):
in str(context.exception)
)

async def test_receive_request(self):
async def test_receive_request_no_offer(self):
cred_ex_record = async_mock.MagicMock()
cred_ex_record.cred_offer = None
cred_request_message = async_mock.MagicMock()

# Not much to assert. Receive request doesn't do anything
# Not much to assert. Receive request doesn't do anything if no prior offer
await self.handler.receive_request(cred_ex_record, cred_request_message)

async def test_receive_request_with_offer_no_id(self):
cred_offer = V20CredOffer(
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_OFFER][
V20CredFormat.Format.LD_PROOF.api
],
)
],
offers_attach=[AttachDecorator.data_base64(LD_PROOF_VC_DETAIL, ident="0")],
)
cred_ex_record = V20CredExRecord(
cred_ex_id="dummy-id",
state=V20CredExRecord.STATE_OFFER_RECEIVED,
cred_offer=cred_offer,
)
cred_request = V20CredRequest(
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][
V20CredFormat.Format.LD_PROOF.api
],
)
],
requests_attach=[
AttachDecorator.data_base64(LD_PROOF_VC_DETAIL, ident="0")
],
)

await self.handler.receive_request(cred_ex_record, cred_request)

async def test_receive_request_with_offer_with_id(self):
detail = deepcopy(LD_PROOF_VC_DETAIL)
detail["credential"]["credentialSubject"]["id"] = "some id"
cred_offer = V20CredOffer(
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_OFFER][
V20CredFormat.Format.LD_PROOF.api
],
)
],
offers_attach=[AttachDecorator.data_base64(detail, ident="0")],
)
cred_ex_record = V20CredExRecord(
cred_ex_id="dummy-id",
state=V20CredExRecord.STATE_OFFER_RECEIVED,
cred_offer=cred_offer,
)
cred_request = V20CredRequest(
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][
V20CredFormat.Format.LD_PROOF.api
],
)
],
requests_attach=[AttachDecorator.data_base64(detail, ident="0")],
)

await self.handler.receive_request(cred_ex_record, cred_request)

async def test_receive_request_with_offer_with_id_x_mismatch_id(self):
detail = deepcopy(LD_PROOF_VC_DETAIL)
detail["credential"]["credentialSubject"]["id"] = "some id"
cred_offer = V20CredOffer(
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_OFFER][
V20CredFormat.Format.LD_PROOF.api
],
)
],
offers_attach=[AttachDecorator.data_base64(detail, ident="0")],
)
cred_ex_record = V20CredExRecord(
cred_ex_id="dummy-id",
state=V20CredExRecord.STATE_OFFER_RECEIVED,
cred_offer=cred_offer,
)
req_detail = deepcopy(detail)
req_detail["credential"]["credentialSubject"]["id"] = "other id"
cred_request = V20CredRequest(
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][
V20CredFormat.Format.LD_PROOF.api
],
)
],
requests_attach=[AttachDecorator.data_base64(req_detail, ident="0")],
)

with self.assertRaises(V20CredFormatError) as context:
await self.handler.receive_request(cred_ex_record, cred_request)
assert "must match offer" in str(context.exception)

async def test_receive_request_with_offer_with_id_x_changed_cred(self):
detail = deepcopy(LD_PROOF_VC_DETAIL)
cred_offer = V20CredOffer(
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_OFFER][
V20CredFormat.Format.LD_PROOF.api
],
)
],
offers_attach=[AttachDecorator.data_base64(detail, ident="0")],
)
cred_ex_record = V20CredExRecord(
cred_ex_id="dummy-id",
state=V20CredExRecord.STATE_OFFER_RECEIVED,
cred_offer=cred_offer,
)
req_detail = deepcopy(LD_PROOF_VC_DETAIL_ED25519_2020)
cred_request = V20CredRequest(
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][
V20CredFormat.Format.LD_PROOF.api
],
)
],
requests_attach=[AttachDecorator.data_base64(req_detail, ident="0")],
)

with self.assertRaises(V20CredFormatError) as context:
await self.handler.receive_request(cred_ex_record, cred_request)
assert "Request must match offer if offer is sent" in str(context.exception)

async def test_issue_credential(self):
cred_request = V20CredRequest(
formats=[
Expand Down