Skip to content

Commit

Permalink
Merge pull request #1599 from shaangill025/did_exchange
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewwhitehead authored Apr 7, 2022
2 parents bcaf61a + 3fae679 commit 8970e5b
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 32 deletions.
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
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
)
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

0 comments on commit 8970e5b

Please sign in to comment.