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

BREAKING: Allow multi-use public invites and public invites with metadata #2034

Merged
Merged
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
19 changes: 18 additions & 1 deletion aries_cloudagent/config/argparse.py
Original file line number Diff line number Diff line change
@@ -1039,7 +1039,17 @@ def add_arguments(self, parser: ArgumentParser):
action="store_true",
env_var="ACAPY_PUBLIC_INVITES",
help=(
"Send invitations out, and receive connection requests, "
"Send invitations out using the public DID for the agent, "
"and receive connection requests solicited by invitations "
"which use the public DID. Default: false."
),
)
parser.add_argument(
"--requests-through-public-did",
action="store_true",
env_var="ACAPY_REQUESTS_THROUGH_PUBLIC_DID",
help=(
"Allow agent to receive unsolicited connection requests, "
"using the public DID for the agent. Default: false."
),
)
@@ -1134,6 +1144,13 @@ def get_settings(self, args: Namespace) -> dict:
settings["monitor_forward"] = args.monitor_forward
if args.public_invites:
settings["public_invites"] = True
if args.requests_through_public_did:
if not args.public_invites:
raise ArgsParseError(
"--public-invites is required to use "
"--requests-through-public-did"
)
settings["requests_through_public_did"] = True
if args.timing:
settings["timing.enabled"] = True
if args.timing_log:
148 changes: 80 additions & 68 deletions aries_cloudagent/protocols/connections/v1_0/manager.py
Original file line number Diff line number Diff line change
@@ -127,10 +127,42 @@ async def create_invitation(
or_default=True,
)
image_url = self.profile.context.settings.get("image_url")
invitation = None
connection = None

invitation_mode = ConnRecord.INVITATION_MODE_ONCE
if multi_use:
invitation_mode = ConnRecord.INVITATION_MODE_MULTI

if not my_label:
my_label = self.profile.settings.get("default_label")

accept = (
ConnRecord.ACCEPT_AUTO
if (
auto_accept
or (
auto_accept is None
and self.profile.settings.get("debug.auto_accept_requests")
)
)
else ConnRecord.ACCEPT_MANUAL
)

if recipient_keys:
# TODO: register recipient keys for relay
# TODO: check that recipient keys are in wallet
invitation_key = recipient_keys[0] # TODO first key appropriate?
else:
# Create and store new invitation key
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
invitation_signing_key = await wallet.create_signing_key(
key_type=ED25519
)
invitation_key = invitation_signing_key.verkey
recipient_keys = [invitation_key]

if public:
if not self.profile.settings.get("public_invites"):
raise ConnectionManagerError("Public invitations are not enabled")
@@ -143,89 +175,64 @@ async def create_invitation(
"Cannot create public invitation with no public DID"
)

if multi_use:
raise ConnectionManagerError(
"Cannot use public and multi_use at the same time"
)

if metadata:
raise ConnectionManagerError(
"Cannot use public and set metadata at the same time"
)

# FIXME - allow ledger instance to format public DID with prefix?
invitation = ConnectionInvitation(
label=my_label, did=f"did:sov:{public_did.did}", image_url=image_url
)

connection = ConnRecord( # create connection record
invitation_key=public_did.verkey,
invitation_msg_id=invitation._id,
invitation_mode=invitation_mode,
their_role=ConnRecord.Role.REQUESTER.rfc23,
state=ConnRecord.State.INVITATION.rfc23,
accept=accept,
alias=alias,
connection_protocol=CONN_PROTO,
)

async with self.profile.session() as session:
await connection.save(session, reason="Created new invitation")

# Add mapping for multitenant relaying.
# Mediation of public keys is not supported yet
await self._route_manager.route_public_did(self.profile, public_did.verkey)

return None, invitation

invitation_mode = ConnRecord.INVITATION_MODE_ONCE
if multi_use:
invitation_mode = ConnRecord.INVITATION_MODE_MULTI

if recipient_keys:
# TODO: register recipient keys for relay
# TODO: check that recipient keys are in wallet
invitation_key = recipient_keys[0] # TODO first key appropriate?
else:
# Create and store new invitation key
# Create connection record
connection = ConnRecord(
invitation_key=invitation_key, # TODO: determine correct key to use
their_role=ConnRecord.Role.REQUESTER.rfc160,
state=ConnRecord.State.INVITATION.rfc160,
accept=accept,
invitation_mode=invitation_mode,
alias=alias,
connection_protocol=CONN_PROTO,
)
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
invitation_signing_key = await wallet.create_signing_key(
key_type=ED25519
)
invitation_key = invitation_signing_key.verkey
recipient_keys = [invitation_key]
await connection.save(session, reason="Created new invitation")

accept = (
ConnRecord.ACCEPT_AUTO
if (
auto_accept
or (
auto_accept is None
and self.profile.settings.get("debug.auto_accept_requests")
)
await self._route_manager.route_invitation(
self.profile, connection, mediation_record
)
routing_keys, my_endpoint = await self._route_manager.routing_info(
self.profile,
my_endpoint or cast(str, self.profile.settings.get("default_endpoint")),
mediation_record,
)
else ConnRecord.ACCEPT_MANUAL
)

# Create connection record
connection = ConnRecord(
invitation_key=invitation_key, # TODO: determine correct key to use
their_role=ConnRecord.Role.REQUESTER.rfc160,
state=ConnRecord.State.INVITATION.rfc160,
accept=accept,
invitation_mode=invitation_mode,
alias=alias,
connection_protocol=CONN_PROTO,
)
async with self.profile.session() as session:
await connection.save(session, reason="Created new invitation")

await self._route_manager.route_invitation(
self.profile, connection, mediation_record
)
routing_keys, my_endpoint = await self._route_manager.routing_info(
self.profile,
my_endpoint or cast(str, self.profile.settings.get("default_endpoint")),
mediation_record,
)
# Create connection invitation message
# Note: Need to split this into two stages
# to support inbound routing of invites
# Would want to reuse create_did_document and convert the result
invitation = ConnectionInvitation(
label=my_label,
recipient_keys=recipient_keys,
routing_keys=routing_keys,
endpoint=my_endpoint,
image_url=image_url,
)

# Create connection invitation message
# Note: Need to split this into two stages to support inbound routing of invites
# Would want to reuse create_did_document and convert the result
invitation = ConnectionInvitation(
label=my_label,
recipient_keys=recipient_keys,
routing_keys=routing_keys,
endpoint=my_endpoint,
image_url=image_url,
)
async with self.profile.session() as session:
await connection.attach_invitation(session, invitation)

@@ -529,6 +536,11 @@ async def receive_request(
their_role=ConnRecord.Role.REQUESTER.rfc160,
)
if not connection:
if not self.profile.settings.get("requests_through_public_did"):
raise ConnectionManagerError(
"Unsolicited connection requests to "
"public DID is not enabled"
)
connection = ConnRecord()
connection.invitation_key = connection_key
connection.my_did = my_info.did
112 changes: 79 additions & 33 deletions aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@
from ....discovery.v2_0.manager import V20DiscoveryMgr

from ..manager import ConnectionManager, ConnectionManagerError
from .. import manager as test_module
from ..messages.connection_invitation import ConnectionInvitation
from ..messages.connection_request import ConnectionRequest
from ..messages.connection_response import ConnectionResponse
@@ -111,21 +112,6 @@ async def setUp(self):
self.manager = ConnectionManager(self.profile)
assert self.manager.profile

async def test_create_invitation_public_and_multi_use_fails(self):
self.context.update_settings({"public_invites": True})
with async_mock.patch.object(
InMemoryWallet, "get_public_did", autospec=True
) as mock_wallet_get_public_did:
mock_wallet_get_public_did.return_value = DIDInfo(
self.test_did,
self.test_verkey,
None,
method=SOV,
key_type=ED25519,
)
with self.assertRaises(ConnectionManagerError):
await self.manager.create_invitation(public=True, multi_use=True)

async def test_create_invitation_non_multi_use_invitation_fails_on_reuse(self):
connect_record, connect_invite = await self.manager.create_invitation()

@@ -173,7 +159,7 @@ async def test_create_invitation_public(self):
public=True, my_endpoint="testendpoint"
)

assert connect_record is None
assert connect_record
assert connect_invite.did.endswith(self.test_did)
self.route_manager.route_public_did.assert_called_once_with(
self.profile, self.test_verkey
@@ -265,23 +251,6 @@ async def test_create_invitation_metadata_assigned(self):

assert await record.metadata_get_all(session) == {"hello": "world"}

async def test_create_invitation_public_and_metadata_fails(self):
self.context.update_settings({"public_invites": True})
with async_mock.patch.object(
InMemoryWallet, "get_public_did", autospec=True
) as mock_wallet_get_public_did:
mock_wallet_get_public_did.return_value = DIDInfo(
self.test_did,
self.test_verkey,
None,
method=SOV,
key_type=ED25519,
)
with self.assertRaises(ConnectionManagerError):
await self.manager.create_invitation(
public=True, metadata={"hello": "world"}
)

async def test_create_invitation_multi_use_metadata_transfers_to_connection(self):
async with self.profile.session() as session:
connect_record, _ = await self.manager.create_invitation(
@@ -642,7 +611,83 @@ async def test_receive_request_public_did_oob_invite(self):
self.profile, mock_request
)

async def test_receive_request_public_did_unsolicited_fails(self):
async with self.profile.session() as session:
mock_request = async_mock.MagicMock()
mock_request.connection = async_mock.MagicMock()
mock_request.connection.did = self.test_did
mock_request.connection.did_doc = async_mock.MagicMock()
mock_request.connection.did_doc.did = self.test_did

receipt = MessageReceipt(
recipient_did=self.test_did, recipient_did_public=True
)
await session.wallet.create_local_did(
method=SOV,
key_type=ED25519,
seed=None,
did=self.test_did,
)

self.context.update_settings({"public_invites": True})
with self.assertRaises(ConnectionManagerError), async_mock.patch.object(
ConnRecord, "connection_id", autospec=True
), async_mock.patch.object(
ConnRecord, "save", autospec=True
) as mock_conn_rec_save, async_mock.patch.object(
ConnRecord, "attach_request", autospec=True
) as mock_conn_attach_request, async_mock.patch.object(
ConnRecord, "retrieve_by_id", autospec=True
) as mock_conn_retrieve_by_id, async_mock.patch.object(
ConnRecord, "retrieve_request", autospec=True
), async_mock.patch.object(
ConnRecord, "retrieve_by_invitation_msg_id", async_mock.CoroutineMock()
) as mock_conn_retrieve_by_invitation_msg_id:
mock_conn_retrieve_by_invitation_msg_id.return_value = None
conn_rec = await self.manager.receive_request(mock_request, receipt)

async def test_receive_request_public_did_conn_invite(self):
async with self.profile.session() as session:
mock_request = async_mock.MagicMock()
mock_request.connection = async_mock.MagicMock()
mock_request.connection.did = self.test_did
mock_request.connection.did_doc = async_mock.MagicMock()
mock_request.connection.did_doc.did = self.test_did

receipt = MessageReceipt(
recipient_did=self.test_did, recipient_did_public=True
)
await session.wallet.create_local_did(
method=SOV,
key_type=ED25519,
seed=None,
did=self.test_did,
)

mock_connection_record = async_mock.MagicMock()
mock_connection_record.save = async_mock.CoroutineMock()
mock_connection_record.attach_request = async_mock.CoroutineMock()

self.context.update_settings({"public_invites": True})
with async_mock.patch.object(
ConnRecord, "connection_id", autospec=True
), async_mock.patch.object(
ConnRecord, "save", autospec=True
) as mock_conn_rec_save, async_mock.patch.object(
ConnRecord, "attach_request", autospec=True
) as mock_conn_attach_request, async_mock.patch.object(
ConnRecord, "retrieve_by_id", autospec=True
) as mock_conn_retrieve_by_id, async_mock.patch.object(
ConnRecord, "retrieve_request", autospec=True
), async_mock.patch.object(
ConnRecord,
"retrieve_by_invitation_msg_id",
async_mock.CoroutineMock(return_value=mock_connection_record),
) as mock_conn_retrieve_by_invitation_msg_id:
conn_rec = await self.manager.receive_request(mock_request, receipt)
assert conn_rec

async def test_receive_request_public_did_unsolicited(self):
async with self.profile.session() as session:
mock_request = async_mock.MagicMock()
mock_request.connection = async_mock.MagicMock()
@@ -661,6 +706,7 @@ async def test_receive_request_public_did_conn_invite(self):
)

self.context.update_settings({"public_invites": True})
self.context.update_settings({"requests_through_public_did": True})
with async_mock.patch.object(
ConnRecord, "connection_id", autospec=True
), async_mock.patch.object(
4 changes: 4 additions & 0 deletions aries_cloudagent/protocols/didexchange/v1_0/manager.py
Original file line number Diff line number Diff line change
@@ -483,6 +483,10 @@ async def receive_request(
)
else:
# request is against implicit invitation on public DID
if not self.profile.settings.get("requests_through_public_did"):
raise DIDXManagerError(
"Unsolicited connection requests to " "public DID is not enabled"
)
async with self.profile.session() as session:
wallet = session.inject(BaseWallet)
my_info = await wallet.create_local_did(
Loading