Skip to content

Commit 748ec9c

Browse files
authored
Merge pull request openwallet-foundation#2034 from Indicio-tech/feat/public-did-multi-use
BREAKING: Allow multi-use public invites and public invites with metadata
2 parents aedcbd3 + bfd920e commit 748ec9c

6 files changed

Lines changed: 338 additions & 111 deletions

File tree

aries_cloudagent/config/argparse.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,17 @@ def add_arguments(self, parser: ArgumentParser):
10391039
action="store_true",
10401040
env_var="ACAPY_PUBLIC_INVITES",
10411041
help=(
1042-
"Send invitations out, and receive connection requests, "
1042+
"Send invitations out using the public DID for the agent, "
1043+
"and receive connection requests solicited by invitations "
1044+
"which use the public DID. Default: false."
1045+
),
1046+
)
1047+
parser.add_argument(
1048+
"--requests-through-public-did",
1049+
action="store_true",
1050+
env_var="ACAPY_REQUESTS_THROUGH_PUBLIC_DID",
1051+
help=(
1052+
"Allow agent to receive unsolicited connection requests, "
10431053
"using the public DID for the agent. Default: false."
10441054
),
10451055
)
@@ -1134,6 +1144,13 @@ def get_settings(self, args: Namespace) -> dict:
11341144
settings["monitor_forward"] = args.monitor_forward
11351145
if args.public_invites:
11361146
settings["public_invites"] = True
1147+
if args.requests_through_public_did:
1148+
if not args.public_invites:
1149+
raise ArgsParseError(
1150+
"--public-invites is required to use "
1151+
"--requests-through-public-did"
1152+
)
1153+
settings["requests_through_public_did"] = True
11371154
if args.timing:
11381155
settings["timing.enabled"] = True
11391156
if args.timing_log:

aries_cloudagent/protocols/connections/v1_0/manager.py

Lines changed: 80 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,42 @@ async def create_invitation(
127127
or_default=True,
128128
)
129129
image_url = self.profile.context.settings.get("image_url")
130+
invitation = None
131+
connection = None
132+
133+
invitation_mode = ConnRecord.INVITATION_MODE_ONCE
134+
if multi_use:
135+
invitation_mode = ConnRecord.INVITATION_MODE_MULTI
130136

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

140+
accept = (
141+
ConnRecord.ACCEPT_AUTO
142+
if (
143+
auto_accept
144+
or (
145+
auto_accept is None
146+
and self.profile.settings.get("debug.auto_accept_requests")
147+
)
148+
)
149+
else ConnRecord.ACCEPT_MANUAL
150+
)
151+
152+
if recipient_keys:
153+
# TODO: register recipient keys for relay
154+
# TODO: check that recipient keys are in wallet
155+
invitation_key = recipient_keys[0] # TODO first key appropriate?
156+
else:
157+
# Create and store new invitation key
158+
async with self.profile.session() as session:
159+
wallet = session.inject(BaseWallet)
160+
invitation_signing_key = await wallet.create_signing_key(
161+
key_type=ED25519
162+
)
163+
invitation_key = invitation_signing_key.verkey
164+
recipient_keys = [invitation_key]
165+
134166
if public:
135167
if not self.profile.settings.get("public_invites"):
136168
raise ConnectionManagerError("Public invitations are not enabled")
@@ -143,89 +175,64 @@ async def create_invitation(
143175
"Cannot create public invitation with no public DID"
144176
)
145177

146-
if multi_use:
147-
raise ConnectionManagerError(
148-
"Cannot use public and multi_use at the same time"
149-
)
150-
151-
if metadata:
152-
raise ConnectionManagerError(
153-
"Cannot use public and set metadata at the same time"
154-
)
155-
156178
# FIXME - allow ledger instance to format public DID with prefix?
157179
invitation = ConnectionInvitation(
158180
label=my_label, did=f"did:sov:{public_did.did}", image_url=image_url
159181
)
160182

183+
connection = ConnRecord( # create connection record
184+
invitation_key=public_did.verkey,
185+
invitation_msg_id=invitation._id,
186+
invitation_mode=invitation_mode,
187+
their_role=ConnRecord.Role.REQUESTER.rfc23,
188+
state=ConnRecord.State.INVITATION.rfc23,
189+
accept=accept,
190+
alias=alias,
191+
connection_protocol=CONN_PROTO,
192+
)
193+
194+
async with self.profile.session() as session:
195+
await connection.save(session, reason="Created new invitation")
196+
161197
# Add mapping for multitenant relaying.
162198
# Mediation of public keys is not supported yet
163199
await self._route_manager.route_public_did(self.profile, public_did.verkey)
164200

165-
return None, invitation
166-
167-
invitation_mode = ConnRecord.INVITATION_MODE_ONCE
168-
if multi_use:
169-
invitation_mode = ConnRecord.INVITATION_MODE_MULTI
170-
171-
if recipient_keys:
172-
# TODO: register recipient keys for relay
173-
# TODO: check that recipient keys are in wallet
174-
invitation_key = recipient_keys[0] # TODO first key appropriate?
175201
else:
176-
# Create and store new invitation key
202+
# Create connection record
203+
connection = ConnRecord(
204+
invitation_key=invitation_key, # TODO: determine correct key to use
205+
their_role=ConnRecord.Role.REQUESTER.rfc160,
206+
state=ConnRecord.State.INVITATION.rfc160,
207+
accept=accept,
208+
invitation_mode=invitation_mode,
209+
alias=alias,
210+
connection_protocol=CONN_PROTO,
211+
)
177212
async with self.profile.session() as session:
178-
wallet = session.inject(BaseWallet)
179-
invitation_signing_key = await wallet.create_signing_key(
180-
key_type=ED25519
181-
)
182-
invitation_key = invitation_signing_key.verkey
183-
recipient_keys = [invitation_key]
213+
await connection.save(session, reason="Created new invitation")
184214

185-
accept = (
186-
ConnRecord.ACCEPT_AUTO
187-
if (
188-
auto_accept
189-
or (
190-
auto_accept is None
191-
and self.profile.settings.get("debug.auto_accept_requests")
192-
)
215+
await self._route_manager.route_invitation(
216+
self.profile, connection, mediation_record
217+
)
218+
routing_keys, my_endpoint = await self._route_manager.routing_info(
219+
self.profile,
220+
my_endpoint or cast(str, self.profile.settings.get("default_endpoint")),
221+
mediation_record,
193222
)
194-
else ConnRecord.ACCEPT_MANUAL
195-
)
196-
197-
# Create connection record
198-
connection = ConnRecord(
199-
invitation_key=invitation_key, # TODO: determine correct key to use
200-
their_role=ConnRecord.Role.REQUESTER.rfc160,
201-
state=ConnRecord.State.INVITATION.rfc160,
202-
accept=accept,
203-
invitation_mode=invitation_mode,
204-
alias=alias,
205-
connection_protocol=CONN_PROTO,
206-
)
207-
async with self.profile.session() as session:
208-
await connection.save(session, reason="Created new invitation")
209223

210-
await self._route_manager.route_invitation(
211-
self.profile, connection, mediation_record
212-
)
213-
routing_keys, my_endpoint = await self._route_manager.routing_info(
214-
self.profile,
215-
my_endpoint or cast(str, self.profile.settings.get("default_endpoint")),
216-
mediation_record,
217-
)
224+
# Create connection invitation message
225+
# Note: Need to split this into two stages
226+
# to support inbound routing of invites
227+
# Would want to reuse create_did_document and convert the result
228+
invitation = ConnectionInvitation(
229+
label=my_label,
230+
recipient_keys=recipient_keys,
231+
routing_keys=routing_keys,
232+
endpoint=my_endpoint,
233+
image_url=image_url,
234+
)
218235

219-
# Create connection invitation message
220-
# Note: Need to split this into two stages to support inbound routing of invites
221-
# Would want to reuse create_did_document and convert the result
222-
invitation = ConnectionInvitation(
223-
label=my_label,
224-
recipient_keys=recipient_keys,
225-
routing_keys=routing_keys,
226-
endpoint=my_endpoint,
227-
image_url=image_url,
228-
)
229236
async with self.profile.session() as session:
230237
await connection.attach_invitation(session, invitation)
231238

@@ -529,6 +536,11 @@ async def receive_request(
529536
their_role=ConnRecord.Role.REQUESTER.rfc160,
530537
)
531538
if not connection:
539+
if not self.profile.settings.get("requests_through_public_did"):
540+
raise ConnectionManagerError(
541+
"Unsolicited connection requests to "
542+
"public DID is not enabled"
543+
)
532544
connection = ConnRecord()
533545
connection.invitation_key = connection_key
534546
connection.my_did = my_info.did

aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from ....discovery.v2_0.manager import V20DiscoveryMgr
3535

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

114-
async def test_create_invitation_public_and_multi_use_fails(self):
115-
self.context.update_settings({"public_invites": True})
116-
with async_mock.patch.object(
117-
InMemoryWallet, "get_public_did", autospec=True
118-
) as mock_wallet_get_public_did:
119-
mock_wallet_get_public_did.return_value = DIDInfo(
120-
self.test_did,
121-
self.test_verkey,
122-
None,
123-
method=SOV,
124-
key_type=ED25519,
125-
)
126-
with self.assertRaises(ConnectionManagerError):
127-
await self.manager.create_invitation(public=True, multi_use=True)
128-
129115
async def test_create_invitation_non_multi_use_invitation_fails_on_reuse(self):
130116
connect_record, connect_invite = await self.manager.create_invitation()
131117

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

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

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

268-
async def test_create_invitation_public_and_metadata_fails(self):
269-
self.context.update_settings({"public_invites": True})
270-
with async_mock.patch.object(
271-
InMemoryWallet, "get_public_did", autospec=True
272-
) as mock_wallet_get_public_did:
273-
mock_wallet_get_public_did.return_value = DIDInfo(
274-
self.test_did,
275-
self.test_verkey,
276-
None,
277-
method=SOV,
278-
key_type=ED25519,
279-
)
280-
with self.assertRaises(ConnectionManagerError):
281-
await self.manager.create_invitation(
282-
public=True, metadata={"hello": "world"}
283-
)
284-
285254
async def test_create_invitation_multi_use_metadata_transfers_to_connection(self):
286255
async with self.profile.session() as session:
287256
connect_record, _ = await self.manager.create_invitation(
@@ -642,7 +611,83 @@ async def test_receive_request_public_did_oob_invite(self):
642611
self.profile, mock_request
643612
)
644613

614+
async def test_receive_request_public_did_unsolicited_fails(self):
615+
async with self.profile.session() as session:
616+
mock_request = async_mock.MagicMock()
617+
mock_request.connection = async_mock.MagicMock()
618+
mock_request.connection.did = self.test_did
619+
mock_request.connection.did_doc = async_mock.MagicMock()
620+
mock_request.connection.did_doc.did = self.test_did
621+
622+
receipt = MessageReceipt(
623+
recipient_did=self.test_did, recipient_did_public=True
624+
)
625+
await session.wallet.create_local_did(
626+
method=SOV,
627+
key_type=ED25519,
628+
seed=None,
629+
did=self.test_did,
630+
)
631+
632+
self.context.update_settings({"public_invites": True})
633+
with self.assertRaises(ConnectionManagerError), async_mock.patch.object(
634+
ConnRecord, "connection_id", autospec=True
635+
), async_mock.patch.object(
636+
ConnRecord, "save", autospec=True
637+
) as mock_conn_rec_save, async_mock.patch.object(
638+
ConnRecord, "attach_request", autospec=True
639+
) as mock_conn_attach_request, async_mock.patch.object(
640+
ConnRecord, "retrieve_by_id", autospec=True
641+
) as mock_conn_retrieve_by_id, async_mock.patch.object(
642+
ConnRecord, "retrieve_request", autospec=True
643+
), async_mock.patch.object(
644+
ConnRecord, "retrieve_by_invitation_msg_id", async_mock.CoroutineMock()
645+
) as mock_conn_retrieve_by_invitation_msg_id:
646+
mock_conn_retrieve_by_invitation_msg_id.return_value = None
647+
conn_rec = await self.manager.receive_request(mock_request, receipt)
648+
645649
async def test_receive_request_public_did_conn_invite(self):
650+
async with self.profile.session() as session:
651+
mock_request = async_mock.MagicMock()
652+
mock_request.connection = async_mock.MagicMock()
653+
mock_request.connection.did = self.test_did
654+
mock_request.connection.did_doc = async_mock.MagicMock()
655+
mock_request.connection.did_doc.did = self.test_did
656+
657+
receipt = MessageReceipt(
658+
recipient_did=self.test_did, recipient_did_public=True
659+
)
660+
await session.wallet.create_local_did(
661+
method=SOV,
662+
key_type=ED25519,
663+
seed=None,
664+
did=self.test_did,
665+
)
666+
667+
mock_connection_record = async_mock.MagicMock()
668+
mock_connection_record.save = async_mock.CoroutineMock()
669+
mock_connection_record.attach_request = async_mock.CoroutineMock()
670+
671+
self.context.update_settings({"public_invites": True})
672+
with async_mock.patch.object(
673+
ConnRecord, "connection_id", autospec=True
674+
), async_mock.patch.object(
675+
ConnRecord, "save", autospec=True
676+
) as mock_conn_rec_save, async_mock.patch.object(
677+
ConnRecord, "attach_request", autospec=True
678+
) as mock_conn_attach_request, async_mock.patch.object(
679+
ConnRecord, "retrieve_by_id", autospec=True
680+
) as mock_conn_retrieve_by_id, async_mock.patch.object(
681+
ConnRecord, "retrieve_request", autospec=True
682+
), async_mock.patch.object(
683+
ConnRecord,
684+
"retrieve_by_invitation_msg_id",
685+
async_mock.CoroutineMock(return_value=mock_connection_record),
686+
) as mock_conn_retrieve_by_invitation_msg_id:
687+
conn_rec = await self.manager.receive_request(mock_request, receipt)
688+
assert conn_rec
689+
690+
async def test_receive_request_public_did_unsolicited(self):
646691
async with self.profile.session() as session:
647692
mock_request = async_mock.MagicMock()
648693
mock_request.connection = async_mock.MagicMock()
@@ -661,6 +706,7 @@ async def test_receive_request_public_did_conn_invite(self):
661706
)
662707

663708
self.context.update_settings({"public_invites": True})
709+
self.context.update_settings({"requests_through_public_did": True})
664710
with async_mock.patch.object(
665711
ConnRecord, "connection_id", autospec=True
666712
), async_mock.patch.object(

aries_cloudagent/protocols/didexchange/v1_0/manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@ async def receive_request(
483483
)
484484
else:
485485
# request is against implicit invitation on public DID
486+
if not self.profile.settings.get("requests_through_public_did"):
487+
raise DIDXManagerError(
488+
"Unsolicited connection requests to " "public DID is not enabled"
489+
)
486490
async with self.profile.session() as session:
487491
wallet = session.inject(BaseWallet)
488492
my_info = await wallet.create_local_did(

0 commit comments

Comments
 (0)