Skip to content

Commit 8970e5b

Browse files
authoredApr 7, 2022
Merge pull request #1599 from shaangill025/did_exchange
2 parents bcaf61a + 3fae679 commit 8970e5b

File tree

6 files changed

+221
-32
lines changed

6 files changed

+221
-32
lines changed
 

Diff for: ‎aries_cloudagent/messaging/decorators/attach_decorator.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ def build_protected(verkey: str):
414414
)
415415
self.jws_ = AttachDecoratorDataJWS.deserialize(jws)
416416

417-
async def verify(self, wallet: BaseWallet) -> bool:
417+
async def verify(self, wallet: BaseWallet, signer_verkey: str = None) -> bool:
418418
"""
419419
Verify the signature(s).
420420
@@ -428,7 +428,7 @@ async def verify(self, wallet: BaseWallet) -> bool:
428428
assert self.jws
429429

430430
b64_payload = unpad(set_urlsafe_b64(self.base64, True))
431-
431+
verkey_to_check = []
432432
for sig in [self.jws] if self.signatures == 1 else self.jws.signatures:
433433
b64_protected = sig.protected
434434
b64_sig = sig.signature
@@ -438,10 +438,18 @@ async def verify(self, wallet: BaseWallet) -> bool:
438438
sign_input = (b64_protected + "." + b64_payload).encode("ascii")
439439
b_sig = b64_to_bytes(b64_sig, urlsafe=True)
440440
verkey = bytes_to_b58(b64_to_bytes(protected["jwk"]["x"], urlsafe=True))
441+
encoded_pk = DIDKey.from_did(protected["jwk"]["kid"]).public_key_b58
442+
verkey_to_check.append(encoded_pk)
441443
if not await wallet.verify_message(
442444
sign_input, b_sig, verkey, KeyType.ED25519
443445
):
444446
return False
447+
if not await wallet.verify_message(
448+
sign_input, b_sig, encoded_pk, KeyType.ED25519
449+
):
450+
return False
451+
if signer_verkey and signer_verkey not in verkey_to_check:
452+
return False
445453
return True
446454

447455
def __eq__(self, other):

Diff for: ‎aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py

+1
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ async def test_indy_sign(self, wallet, seed):
475475
assert deco_indy.data.header_map()["kid"] == did_key(did_info[0].verkey)
476476
assert deco_indy.data.header_map()["jwk"]["kid"] == did_key(did_info[0].verkey)
477477
assert await deco_indy.data.verify(wallet)
478+
assert await deco_indy.data.verify(wallet, did_info[0].verkey)
478479

479480
indy_cred = json.loads(deco_indy.data.signed.decode())
480481
assert indy_cred == INDY_CRED

Diff for: ‎aries_cloudagent/protocols/connections/v1_0/manager.py

+11
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,17 @@ async def accept_response(
835835
)
836836
if their_did != conn_did_doc.did:
837837
raise ConnectionManagerError("Connection DID does not match DIDDoc id")
838+
# Verify connection response using connection field
839+
async with self.profile.session() as session:
840+
wallet = session.inject(BaseWallet)
841+
try:
842+
await response.verify_signed_field(
843+
"connection", wallet, connection.invitation_key
844+
)
845+
except ValueError:
846+
raise ConnectionManagerError(
847+
"connection field verification using invitation_key failed"
848+
)
838849
await self.store_did_document(conn_did_doc)
839850

840851
connection.their_did = their_did

Diff for: ‎aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py

+43-2
Original file line numberDiff line numberDiff line change
@@ -1277,7 +1277,9 @@ async def test_accept_response_find_by_thread_id(self):
12771277
mock_response.connection.did = self.test_target_did
12781278
mock_response.connection.did_doc = async_mock.MagicMock()
12791279
mock_response.connection.did_doc.did = self.test_target_did
1280-
1280+
mock_response.verify_signed_field = async_mock.CoroutineMock(
1281+
return_value="sig_verkey"
1282+
)
12811283
receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True)
12821284

12831285
with async_mock.patch.object(
@@ -1294,6 +1296,7 @@ async def test_accept_response_find_by_thread_id(self):
12941296
save=async_mock.CoroutineMock(),
12951297
metadata_get=async_mock.CoroutineMock(),
12961298
connection_id="test-conn-id",
1299+
invitation_key="test-invitation-key",
12971300
)
12981301
conn_rec = await self.manager.accept_response(mock_response, receipt)
12991302
assert conn_rec.their_did == self.test_target_did
@@ -1306,6 +1309,9 @@ async def test_accept_response_not_found_by_thread_id_receipt_has_sender_did(sel
13061309
mock_response.connection.did = self.test_target_did
13071310
mock_response.connection.did_doc = async_mock.MagicMock()
13081311
mock_response.connection.did_doc.did = self.test_target_did
1312+
mock_response.verify_signed_field = async_mock.CoroutineMock(
1313+
return_value="sig_verkey"
1314+
)
13091315

13101316
receipt = MessageReceipt(sender_did=self.test_target_did)
13111317

@@ -1326,6 +1332,7 @@ async def test_accept_response_not_found_by_thread_id_receipt_has_sender_did(sel
13261332
save=async_mock.CoroutineMock(),
13271333
metadata_get=async_mock.CoroutineMock(return_value=False),
13281334
connection_id="test-conn-id",
1335+
invitation_key="test-invitation-id",
13291336
)
13301337

13311338
conn_rec = await self.manager.accept_response(mock_response, receipt)
@@ -1426,14 +1433,47 @@ async def test_accept_response_find_by_thread_id_did_mismatch(self):
14261433
with self.assertRaises(ConnectionManagerError):
14271434
await self.manager.accept_response(mock_response, receipt)
14281435

1429-
async def test_accept_response_auto_send_mediation_request(self):
1436+
async def test_accept_response_verify_invitation_key_sign_failure(self):
14301437
mock_response = async_mock.MagicMock()
14311438
mock_response._thread = async_mock.MagicMock()
14321439
mock_response.connection = async_mock.MagicMock()
14331440
mock_response.connection.did = self.test_target_did
14341441
mock_response.connection.did_doc = async_mock.MagicMock()
14351442
mock_response.connection.did_doc.did = self.test_target_did
1443+
mock_response.verify_signed_field = async_mock.CoroutineMock(
1444+
side_effect=ValueError
1445+
)
1446+
receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True)
1447+
1448+
with async_mock.patch.object(
1449+
ConnRecord, "save", autospec=True
1450+
) as mock_conn_rec_save, async_mock.patch.object(
1451+
ConnRecord, "retrieve_by_request_id", async_mock.CoroutineMock()
1452+
) as mock_conn_retrieve_by_req_id, async_mock.patch.object(
1453+
MediationManager, "get_default_mediator", async_mock.CoroutineMock()
1454+
):
1455+
mock_conn_retrieve_by_req_id.return_value = async_mock.MagicMock(
1456+
did=self.test_target_did,
1457+
did_doc=async_mock.MagicMock(did=self.test_target_did),
1458+
state=ConnRecord.State.RESPONSE.rfc23,
1459+
save=async_mock.CoroutineMock(),
1460+
metadata_get=async_mock.CoroutineMock(),
1461+
connection_id="test-conn-id",
1462+
invitation_key="test-invitation-key",
1463+
)
1464+
with self.assertRaises(ConnectionManagerError):
1465+
await self.manager.accept_response(mock_response, receipt)
14361466

1467+
async def test_accept_response_auto_send_mediation_request(self):
1468+
mock_response = async_mock.MagicMock()
1469+
mock_response._thread = async_mock.MagicMock()
1470+
mock_response.connection = async_mock.MagicMock()
1471+
mock_response.connection.did = self.test_target_did
1472+
mock_response.connection.did_doc = async_mock.MagicMock()
1473+
mock_response.connection.did_doc.did = self.test_target_did
1474+
mock_response.verify_signed_field = async_mock.CoroutineMock(
1475+
return_value="sig_verkey"
1476+
)
14371477
receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True)
14381478

14391479
with async_mock.patch.object(
@@ -1450,6 +1490,7 @@ async def test_accept_response_auto_send_mediation_request(self):
14501490
save=async_mock.CoroutineMock(),
14511491
metadata_get=async_mock.CoroutineMock(return_value=True),
14521492
connection_id="test-conn-id",
1493+
invitation_key="test-invitation-key",
14531494
)
14541495
conn_rec = await self.manager.accept_response(mock_response, receipt)
14551496
assert conn_rec.their_did == self.test_target_did

Diff for: ‎aries_cloudagent/protocols/didexchange/v1_0/manager.py

+62-13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import json
44
import logging
5+
import pydid
6+
7+
from pydid import BaseDIDDocument as ResolvedDocument, DIDCommService
58

69
from ....connections.models.conn_record import ConnRecord
710
from ....connections.models.diddoc import DIDDoc
@@ -12,6 +15,8 @@
1215
from ....messaging.decorators.attach_decorator import AttachDecorator
1316
from ....messaging.responder import BaseResponder
1417
from ....multitenant.base import BaseMultitenantManager
18+
from ....resolver.base import ResolverError
19+
from ....resolver.did_resolver import DIDResolver
1520
from ....storage.error import StorageNotFoundError
1621
from ....transport.inbound.receipt import MessageReceipt
1722
from ....wallet.base import BaseWallet
@@ -141,7 +146,13 @@ async def receive_invitation(
141146

142147
# Save the invitation for later processing
143148
await conn_rec.attach_invitation(session, invitation)
144-
149+
if not conn_rec.invitation_key and conn_rec.their_public_did:
150+
did_document = await self.get_resolved_did_document(
151+
conn_rec.their_public_did
152+
)
153+
conn_rec.invitation_key = did_document.verification_method[
154+
0
155+
].public_key_base58
145156
if conn_rec.accept == ConnRecord.ACCEPT_AUTO:
146157
request = await self.create_request(conn_rec, mediation_id=mediation_id)
147158
responder = self.profile.inject_or(BaseResponder)
@@ -296,14 +307,11 @@ async def create_request(
296307
filter(None, [base_mediation_record, mediation_record])
297308
),
298309
)
299-
if (
300-
conn_rec.their_public_did is not None
301-
and conn_rec.their_public_did.startswith("did:")
302-
):
310+
if conn_rec.their_public_did is not None:
303311
qualified_did = conn_rec.their_public_did
304-
else:
305-
qualified_did = f"did:sov:{conn_rec.their_public_did}"
306-
pthid = conn_rec.invitation_msg_id or qualified_did
312+
did_document = await self.get_resolved_did_document(qualified_did)
313+
did_url = await self.get_first_applicable_didcomm_service(did_document)
314+
pthid = conn_rec.invitation_msg_id or did_url
307315
attach = AttachDecorator.data_base64(did_doc.serialize())
308316
async with self.profile.session() as session:
309317
wallet = session.inject(BaseWallet)
@@ -468,9 +476,7 @@ async def receive_request(
468476
)
469477
async with self.profile.session() as session:
470478
wallet = session.inject(BaseWallet)
471-
if not await request.did_doc_attach.data.verify(wallet):
472-
raise DIDXManagerError("DID Doc signature failed verification")
473-
conn_did_doc = DIDDoc.from_json(request.did_doc_attach.data.signed.decode())
479+
conn_did_doc = await self.verify_diddoc(wallet, request.did_doc_attach)
474480
if request.did != conn_did_doc.did:
475481
raise DIDXManagerError(
476482
(
@@ -751,7 +757,9 @@ async def accept_response(
751757
raise DIDXManagerError("No DIDDoc attached; cannot connect to public DID")
752758
async with self.profile.session() as session:
753759
wallet = session.inject(BaseWallet)
754-
conn_did_doc = await self.verify_diddoc(wallet, response.did_doc_attach)
760+
conn_did_doc = await self.verify_diddoc(
761+
wallet, response.did_doc_attach, conn_rec.invitation_key
762+
)
755763
if their_did != conn_did_doc.did:
756764
raise DIDXManagerError(
757765
f"Connection DID {their_did} "
@@ -861,12 +869,53 @@ async def verify_diddoc(
861869
self,
862870
wallet: BaseWallet,
863871
attached: AttachDecorator,
872+
invi_key: str = None,
864873
) -> DIDDoc:
865874
"""Verify DIDDoc attachment and return signed data."""
866875
signed_diddoc_bytes = attached.data.signed
867876
if not signed_diddoc_bytes:
868877
raise DIDXManagerError("DID doc attachment is not signed.")
869-
if not await attached.data.verify(wallet):
878+
if not await attached.data.verify(wallet, invi_key):
870879
raise DIDXManagerError("DID doc attachment signature failed verification")
871880

872881
return DIDDoc.deserialize(json.loads(signed_diddoc_bytes.decode()))
882+
883+
async def get_resolved_did_document(self, qualified_did: str) -> ResolvedDocument:
884+
"""Return resolved DID document."""
885+
resolver = self._profile.inject(DIDResolver)
886+
if not qualified_did.startswith("did:"):
887+
qualified_did = f"did:sov:{qualified_did}"
888+
try:
889+
doc_dict: dict = await resolver.resolve(self._profile, qualified_did)
890+
doc = pydid.deserialize_document(doc_dict, strict=True)
891+
return doc
892+
except ResolverError as error:
893+
raise DIDXManagerError(
894+
"Failed to resolve public DID in invitation"
895+
) from error
896+
897+
async def get_first_applicable_didcomm_service(
898+
self, did_doc: ResolvedDocument
899+
) -> str:
900+
"""Return first applicable DIDComm service url with highest priority."""
901+
if not did_doc.service:
902+
raise DIDXManagerError(
903+
"Cannot connect via public DID that has no associated services"
904+
)
905+
906+
didcomm_services = sorted(
907+
[
908+
service
909+
for service in did_doc.service
910+
if isinstance(service, DIDCommService)
911+
],
912+
key=lambda service: service.priority,
913+
)
914+
915+
if not didcomm_services:
916+
raise DIDXManagerError(
917+
"Cannot connect via public DID that has no associated DIDComm services"
918+
)
919+
920+
first_didcomm_service, *_ = didcomm_services
921+
return first_didcomm_service.id

0 commit comments

Comments
 (0)
Please sign in to comment.