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

did-exchange implicit request pthid update & invitation key verification #1599

Merged
merged 30 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6103967
update pthid
shaangill025 Jan 13, 2022
dcb5cfd
format fix
shaangill025 Jan 13, 2022
a142c9d
updates
shaangill025 Jan 18, 2022
6ea511f
Merge branch 'main' into did_exchange
shaangill025 Jan 20, 2022
acac74d
Merge branch 'main' into did_exchange
shaangill025 Jan 21, 2022
4c1a1df
Merge branch 'main' into did_exchange
swcurran Feb 8, 2022
9c85423
Merge branch 'main' into did_exchange
shaangill025 Feb 8, 2022
9da4918
Merge branch 'main' into did_exchange
shaangill025 Mar 18, 2022
a26ff55
Merge branch 'main' of https://github.com/hyperledger/aries-cloudagen…
shaangill025 Mar 22, 2022
3ecbdeb
Merge branch 'did_exchange' of https://github.com/shaangill025/aries-…
shaangill025 Mar 22, 2022
0b62eb3
verify signature from kid
shaangill025 Mar 24, 2022
790ab5e
Merge branch 'main' of https://github.com/hyperledger/aries-cloudagen…
shaangill025 Mar 24, 2022
b661cc0
Merge branch 'main' into did_exchange
shaangill025 Mar 29, 2022
aabb96e
Merge branch 'main' of https://github.com/hyperledger/aries-cloudagen…
shaangill025 Mar 30, 2022
84e407e
Merge branch 'did_exchange' of https://github.com/shaangill025/aries-…
shaangill025 Mar 30, 2022
7a9295e
use ursa-bbs-signatures 1.0.1
shaangill025 Mar 30, 2022
3849d51
Merge branch 'main' of https://github.com/hyperledger/aries-cloudagen…
shaangill025 Mar 30, 2022
ed19dcc
revert locking ursa-bbs-signature package - 1.0.2 pypi removed
shaangill025 Mar 30, 2022
29736df
Merge branch 'main' into did_exchange
shaangill025 Mar 31, 2022
ff5337e
Merge branch 'main' into did_exchange
shaangill025 Apr 1, 2022
9c998e0
Merge branch 'main' of https://github.com/hyperledger/aries-cloudagen…
shaangill025 Apr 1, 2022
2afaae8
Merge branch 'did_exchange' of https://github.com/shaangill025/aries-…
shaangill025 Apr 1, 2022
91d49be
retrigger check
shaangill025 Apr 1, 2022
ab22fcb
Merge branch 'main' into did_exchange
ianco Apr 5, 2022
96a0404
Merge branch 'main' into did_exchange
ianco Apr 5, 2022
40ae949
Merge branch 'main' into did_exchange
ianco Apr 6, 2022
9ce96be
update attach_decorator verify method
shaangill025 Apr 6, 2022
e11f021
Merge branch 'main' of https://github.com/hyperledger/aries-cloudagen…
shaangill025 Apr 6, 2022
1fbc77a
Merge branch 'did_exchange' of https://github.com/shaangill025/aries-…
shaangill025 Apr 6, 2022
3fae679
Merge branch 'main' into did_exchange
shaangill025 Apr 7, 2022
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
12 changes: 10 additions & 2 deletions aries_cloudagent/messaging/decorators/attach_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ def build_protected(verkey: str):
)
self.jws_ = AttachDecoratorDataJWS.deserialize(jws)

async def verify(self, wallet: BaseWallet) -> bool:
async def verify(self, wallet: BaseWallet, signer_verkey: str = None) -> bool:
"""
Verify the signature(s).

Expand All @@ -428,7 +428,7 @@ async def verify(self, wallet: BaseWallet) -> bool:
assert self.jws

b64_payload = unpad(set_urlsafe_b64(self.base64, True))

verkey_to_check = []
for sig in [self.jws] if self.signatures == 1 else self.jws.signatures:
b64_protected = sig.protected
b64_sig = sig.signature
Expand All @@ -438,10 +438,18 @@ async def verify(self, wallet: BaseWallet) -> bool:
sign_input = (b64_protected + "." + b64_payload).encode("ascii")
b_sig = b64_to_bytes(b64_sig, urlsafe=True)
verkey = bytes_to_b58(b64_to_bytes(protected["jwk"]["x"], urlsafe=True))
encoded_pk = DIDKey.from_did(protected["jwk"]["kid"]).public_key_b58
verkey_to_check.append(encoded_pk)
if not await wallet.verify_message(
sign_input, b_sig, verkey, KeyType.ED25519
):
return False
if not await wallet.verify_message(
sign_input, b_sig, encoded_pk, KeyType.ED25519
):
return False
if signer_verkey and signer_verkey not in verkey_to_check:
return False
shaangill025 marked this conversation as resolved.
Show resolved Hide resolved
return True

def __eq__(self, other):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ async def test_indy_sign(self, wallet, seed):
assert deco_indy.data.header_map()["kid"] == did_key(did_info[0].verkey)
assert deco_indy.data.header_map()["jwk"]["kid"] == did_key(did_info[0].verkey)
assert await deco_indy.data.verify(wallet)
assert await deco_indy.data.verify(wallet, did_info[0].verkey)

indy_cred = json.loads(deco_indy.data.signed.decode())
assert indy_cred == INDY_CRED
Expand Down
11 changes: 11 additions & 0 deletions aries_cloudagent/protocols/connections/v1_0/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,17 @@ async def accept_response(
)
if their_did != conn_did_doc.did:
raise ConnectionManagerError("Connection DID does not match DIDDoc id")
# Verify connection response using connection field
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
try:
await response.verify_signed_field(
"connection", wallet, connection.invitation_key
)
except ValueError:
raise ConnectionManagerError(
"connection field verification using invitation_key failed"
)
await self.store_did_document(conn_did_doc)

connection.their_did = their_did
Expand Down
45 changes: 43 additions & 2 deletions aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1277,7 +1277,9 @@ async def test_accept_response_find_by_thread_id(self):
mock_response.connection.did = self.test_target_did
mock_response.connection.did_doc = async_mock.MagicMock()
mock_response.connection.did_doc.did = self.test_target_did

mock_response.verify_signed_field = async_mock.CoroutineMock(
return_value="sig_verkey"
)
receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True)

with async_mock.patch.object(
Expand All @@ -1294,6 +1296,7 @@ async def test_accept_response_find_by_thread_id(self):
save=async_mock.CoroutineMock(),
metadata_get=async_mock.CoroutineMock(),
connection_id="test-conn-id",
invitation_key="test-invitation-key",
)
conn_rec = await self.manager.accept_response(mock_response, receipt)
assert conn_rec.their_did == self.test_target_did
Expand All @@ -1306,6 +1309,9 @@ async def test_accept_response_not_found_by_thread_id_receipt_has_sender_did(sel
mock_response.connection.did = self.test_target_did
mock_response.connection.did_doc = async_mock.MagicMock()
mock_response.connection.did_doc.did = self.test_target_did
mock_response.verify_signed_field = async_mock.CoroutineMock(
return_value="sig_verkey"
)

receipt = MessageReceipt(sender_did=self.test_target_did)

Expand All @@ -1326,6 +1332,7 @@ async def test_accept_response_not_found_by_thread_id_receipt_has_sender_did(sel
save=async_mock.CoroutineMock(),
metadata_get=async_mock.CoroutineMock(return_value=False),
connection_id="test-conn-id",
invitation_key="test-invitation-id",
)

conn_rec = await self.manager.accept_response(mock_response, receipt)
Expand Down Expand Up @@ -1426,14 +1433,47 @@ async def test_accept_response_find_by_thread_id_did_mismatch(self):
with self.assertRaises(ConnectionManagerError):
await self.manager.accept_response(mock_response, receipt)

async def test_accept_response_auto_send_mediation_request(self):
async def test_accept_response_verify_invitation_key_sign_failure(self):
mock_response = async_mock.MagicMock()
mock_response._thread = async_mock.MagicMock()
mock_response.connection = async_mock.MagicMock()
mock_response.connection.did = self.test_target_did
mock_response.connection.did_doc = async_mock.MagicMock()
mock_response.connection.did_doc.did = self.test_target_did
mock_response.verify_signed_field = async_mock.CoroutineMock(
side_effect=ValueError
)
receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True)

with async_mock.patch.object(
ConnRecord, "save", autospec=True
) as mock_conn_rec_save, async_mock.patch.object(
ConnRecord, "retrieve_by_request_id", async_mock.CoroutineMock()
) as mock_conn_retrieve_by_req_id, async_mock.patch.object(
MediationManager, "get_default_mediator", async_mock.CoroutineMock()
):
mock_conn_retrieve_by_req_id.return_value = async_mock.MagicMock(
did=self.test_target_did,
did_doc=async_mock.MagicMock(did=self.test_target_did),
state=ConnRecord.State.RESPONSE.rfc23,
save=async_mock.CoroutineMock(),
metadata_get=async_mock.CoroutineMock(),
connection_id="test-conn-id",
invitation_key="test-invitation-key",
)
with self.assertRaises(ConnectionManagerError):
await self.manager.accept_response(mock_response, receipt)

async def test_accept_response_auto_send_mediation_request(self):
mock_response = async_mock.MagicMock()
mock_response._thread = async_mock.MagicMock()
mock_response.connection = async_mock.MagicMock()
mock_response.connection.did = self.test_target_did
mock_response.connection.did_doc = async_mock.MagicMock()
mock_response.connection.did_doc.did = self.test_target_did
mock_response.verify_signed_field = async_mock.CoroutineMock(
return_value="sig_verkey"
)
receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True)

with async_mock.patch.object(
Expand All @@ -1450,6 +1490,7 @@ async def test_accept_response_auto_send_mediation_request(self):
save=async_mock.CoroutineMock(),
metadata_get=async_mock.CoroutineMock(return_value=True),
connection_id="test-conn-id",
invitation_key="test-invitation-key",
)
conn_rec = await self.manager.accept_response(mock_response, receipt)
assert conn_rec.their_did == self.test_target_did
Expand Down
75 changes: 62 additions & 13 deletions aries_cloudagent/protocols/didexchange/v1_0/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import json
import logging
import pydid

from pydid import BaseDIDDocument as ResolvedDocument, DIDCommService

from ....connections.models.conn_record import ConnRecord
from ....connections.models.diddoc import DIDDoc
Expand All @@ -12,6 +15,8 @@
from ....messaging.decorators.attach_decorator import AttachDecorator
from ....messaging.responder import BaseResponder
from ....multitenant.base import BaseMultitenantManager
from ....resolver.base import ResolverError
from ....resolver.did_resolver import DIDResolver
from ....storage.error import StorageNotFoundError
from ....transport.inbound.receipt import MessageReceipt
from ....wallet.base import BaseWallet
Expand Down Expand Up @@ -141,7 +146,13 @@ async def receive_invitation(

# Save the invitation for later processing
await conn_rec.attach_invitation(session, invitation)

if not conn_rec.invitation_key and conn_rec.their_public_did:
did_document = await self.get_resolved_did_document(
conn_rec.their_public_did
)
conn_rec.invitation_key = did_document.verification_method[
0
].public_key_base58
if conn_rec.accept == ConnRecord.ACCEPT_AUTO:
request = await self.create_request(conn_rec, mediation_id=mediation_id)
responder = self.profile.inject_or(BaseResponder)
Expand Down Expand Up @@ -296,14 +307,11 @@ async def create_request(
filter(None, [base_mediation_record, mediation_record])
),
)
if (
conn_rec.their_public_did is not None
and conn_rec.their_public_did.startswith("did:")
):
if conn_rec.their_public_did is not None:
qualified_did = conn_rec.their_public_did
else:
qualified_did = f"did:sov:{conn_rec.their_public_did}"
pthid = conn_rec.invitation_msg_id or qualified_did
did_document = await self.get_resolved_did_document(qualified_did)
did_url = await self.get_first_applicable_didcomm_service(did_document)
pthid = conn_rec.invitation_msg_id or did_url
attach = AttachDecorator.data_base64(did_doc.serialize())
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
Expand Down Expand Up @@ -468,9 +476,7 @@ async def receive_request(
)
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
if not await request.did_doc_attach.data.verify(wallet):
raise DIDXManagerError("DID Doc signature failed verification")
conn_did_doc = DIDDoc.from_json(request.did_doc_attach.data.signed.decode())
conn_did_doc = await self.verify_diddoc(wallet, request.did_doc_attach)
if request.did != conn_did_doc.did:
raise DIDXManagerError(
(
Expand Down Expand Up @@ -751,7 +757,9 @@ async def accept_response(
raise DIDXManagerError("No DIDDoc attached; cannot connect to public DID")
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
conn_did_doc = await self.verify_diddoc(wallet, response.did_doc_attach)
conn_did_doc = await self.verify_diddoc(
wallet, response.did_doc_attach, conn_rec.invitation_key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checks whether the did document is signed with the first verification method (code above in this file assign conn_rec.invitation_key to first verificationMethod). I think this should be signed using an authentication key. Also I'm not sure which key we should check (the RFC is not very clear), but it could be signed with the second (or third) authentication key I guess.

Maybe @andrewwhitehead has some insights on this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based upon the clarification received during Aries WG 2022/03/23, updated verify method and now using jws.header.kid to verify signature.

)
if their_did != conn_did_doc.did:
raise DIDXManagerError(
f"Connection DID {their_did} "
Expand Down Expand Up @@ -861,12 +869,53 @@ async def verify_diddoc(
self,
wallet: BaseWallet,
attached: AttachDecorator,
invi_key: str = None,
) -> DIDDoc:
"""Verify DIDDoc attachment and return signed data."""
signed_diddoc_bytes = attached.data.signed
if not signed_diddoc_bytes:
raise DIDXManagerError("DID doc attachment is not signed.")
if not await attached.data.verify(wallet):
if not await attached.data.verify(wallet, invi_key):
raise DIDXManagerError("DID doc attachment signature failed verification")

return DIDDoc.deserialize(json.loads(signed_diddoc_bytes.decode()))

async def get_resolved_did_document(self, qualified_did: str) -> ResolvedDocument:
"""Return resolved DID document."""
resolver = self._profile.inject(DIDResolver)
if not qualified_did.startswith("did:"):
qualified_did = f"did:sov:{qualified_did}"
try:
doc_dict: dict = await resolver.resolve(self._profile, qualified_did)
doc = pydid.deserialize_document(doc_dict, strict=True)
return doc
except ResolverError as error:
raise DIDXManagerError(
"Failed to resolve public DID in invitation"
) from error

async def get_first_applicable_didcomm_service(
self, did_doc: ResolvedDocument
) -> str:
"""Return first applicable DIDComm service url with highest priority."""
if not did_doc.service:
raise DIDXManagerError(
"Cannot connect via public DID that has no associated services"
)

didcomm_services = sorted(
[
service
for service in did_doc.service
if isinstance(service, DIDCommService)
],
key=lambda service: service.priority,
)

if not didcomm_services:
raise DIDXManagerError(
"Cannot connect via public DID that has no associated DIDComm services"
)

first_didcomm_service, *_ = didcomm_services
return first_didcomm_service.id
Loading