aries-cloudagent-python
aries-cloudagent-python copied to clipboard
oob invitation with credential-offer attachment failed.
I'm using aca-py 0.8.2 as issuer and holder agent.
When holder agent received out-of-band invitation with credential-offer attachment from issuer, holder agent can not proceed attachement because new connection is not completed at that moment (connection state was request state). connection state was changed to active after attachement process failed.
The strange thing is that when I look at the log of event_bus, the connection was already active before the error occurred.
Reproduction steps
issuer side:
- create credential-offer with API /issue-credential/create-offer
- create oob invitation with API /out-of-band/create-invitation?auto_accept=true
- attachments: [ { type: "credential-offer", id: <cred_ex_id from step 1>} ]
- send oob invitation to holder side
holder side:
- receive oob invitation with API /out-of-band/receive-invitation?auto_accept=true&use_existing_connection=false
Error message
aries_cloudagent.core.dispatcher ERROR Handler error: invitation_receive
Traceback (most recent call last):
File ".../asyncio/tasks.py", line 180, in __step
result = coro.send(None)
File ".../aries_cloudagent/protocols/out_of_band/v1_0/routes.py", line 244, in invitation_receive
result = await oob_mgr.receive_invitation(
File ".../aries_cloudagent/protocols/out_of_band/v1_0/manager.py", line 534, in receive_invitation
"Connection not ready to process attach message "
aries_cloudagent.protocols.out_of_band.v1_0.manager.OutOfBandManagerError: Connection not ready to process attach message for connection_id: 22316045-c742-46a9-98ad-b832230d67d4 and invitation_msg_id a4aba6cf-feef-4d6b-8df5-838e7628292a
aries_cloudagent.admin.server ERROR Handler error with exception: Connection not ready to process attach message for connection_id: 22316045-c742-46a9-98ad-b832230d67d4 and invitation_msg_id a4aba6cf-feef-4d6b-8df5-838e7628292a
This patch eliminates the error. But I don't think this can ever be a solution. However, I hope this helps you solve this issue.
diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/manager.py b/aries_cloudagent/protocols/out_of_band/v1_0/manager.py
index 4cf530a0e..b3429679c 100644
--- a/aries_cloudagent/protocols/out_of_band/v1_0/manager.py
+++ b/aries_cloudagent/protocols/out_of_band/v1_0/manager.py
@@ -700,8 +700,15 @@ class OutOfBandManager(BaseConnectionManager):
)
except asyncio.TimeoutError:
- LOGGER.warning(f"Connection for connection_id {connection_id} not ready")
- return None
+ async with self.profile.session() as session:
+ conn_record = await ConnRecord.retrieve_by_id(
+ session, connection_id
+ )
+ if conn_record.is_ready:
+ return conn_record
+ else:
+ LOGGER.warning(f"Connection for connection_id {connection_id} not ready")
+ return None
async def _handle_hanshake_reuse(
self, oob_record: OobRecord, conn_record: ConnRecord, version: str
Didn't get very far with this, but followed the above steps and duplicated the error.
In the devcontainer, running "start acapy" launch configuration (multitenant), I ran this script to duplicate. This should get someone up and running in minutes to investigate. There is a note/comment in the above area of code that this logic should be off loaded to an actual event handler and obviously the current implementation does not work.
import time
import uuid
import requests
FABER_ADMIN_URL = "http://localhost:9011"
ALICE_ADMIN_URL = "http://localhost:9012"
ACME_ADMIN_URL = "http://localhost:9013"
MULTI_ADMIN_URL = "http://localhost:9061"
LEDGER_URL = " http://test.bcovrin.vonx.io"
def wait_a_bit(secs: int = 1):
total = secs
print(f"... wait {total} seconds ...")
time.sleep(total)
def create_public_did(alias, url, headers=None):
print("\ncreate_public_did")
if headers is None:
headers = {}
response = requests.post(f"{url}/wallet/did/create", headers=headers)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
did = json["result"]["did"]
verkey = json["result"]["verkey"]
data = {"alias": alias, "did": did, "verkey": verkey, "role": "TRUST_ANCHOR"}
response = requests.post(f"{LEDGER_URL}/register", json=data)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
wait_a_bit(1)
response = requests.post(f"{url}/wallet/did/public?did={did}", headers=headers)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
wait_a_bit(1)
response = requests.get(f"{url}/wallet/did/public", headers=headers)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
return did
def create_offer(cred_def_id, data, url, headers=None):
print("\create_offer")
if headers is None:
headers = {}
if data is None:
cred_preview = {
"@type": "https://didcomm.org/issue-credential/2.0/credential-preview",
"attributes": [
{"name": "name", "value": "Joe"},
{"name": "age", "value": "30"},
],
}
data = {
"comment": f"Offer on cred def id {cred_def_id}",
"auto_remove": False,
"credential_preview": cred_preview,
"filter": {"indy": {"cred_def_id": cred_def_id}},
}
response = requests.post(
f"{url}/issue-credential-2.0/create-offer", headers=headers, json=data
)
print(f"response = {response}")
json = response.json()
# print(f"json = {json}")
return json
def create_oob_invitation_with_offer(
my_label, alias, cred_ex_id, mediation_id, url, headers=None
):
print("\create_oob_invitation")
if headers is None:
headers = {}
data = {
"my_label": my_label,
"handshake_protocols": ["rfc23"],
"use_public_did": "true",
"attachments": [{"type": "credential-offer", "id": cred_ex_id}],
}
if mediation_id:
data["mediation_id"] = mediation_id
params = {
"alias": alias,
"auto_accept": "false",
}
response = requests.post(
f"{url}/out-of-band/create-invitation",
headers=headers,
params=params,
json=data,
)
print(f"response = {response}")
json = response.json()
print(f"create-oob-invitation json = {json}")
invitation = json["invitation"]
oob_id = json["oob_id"]
return invitation, oob_id
def receive_oob_invitation(invitation, alias, mediation_id, url, headers=None):
print("\receive_oob_invitation")
if headers is None:
headers = {}
data = invitation
params = {
"alias": alias,
"auto_accept": "true",
}
if mediation_id:
params["mediation_id"] = mediation_id
response = requests.post(
f"{url}/out-of-band/receive-invitation",
headers=headers,
json=data,
params=params,
)
print(f"response = {response}")
json = response.json()
print(f"receive_oob_invitation json = {json}")
connection_id = json["connection_id"]
return connection_id
def create_invitation(my_label, alias, mediation_id, url, headers=None):
print("\ncreate_invitation")
if headers is None:
headers = {}
data = {
"my_label": my_label,
}
if mediation_id:
data["mediation_id"] = mediation_id
params = {
"alias": alias,
"auto_accept": "true",
}
response = requests.post(
f"{url}/connections/create-invitation",
headers=headers,
params=params,
json=data,
)
print(f"response = {response}")
json = response.json()
print(f"create-invitation json = {json}")
invitation = json["invitation"]
connection_id = json["connection_id"]
recipient_keys = json["invitation"]["recipientKeys"]
return invitation, connection_id, recipient_keys
def receive_invitation(invitation, alias, mediation_id, url, headers=None):
print("\nreceive_invitation")
if headers is None:
headers = {}
data = invitation
params = {
"alias": alias,
"auto_accept": "true",
}
if mediation_id:
params["mediation_id"] = mediation_id
response = requests.post(
f"{url}/connections/receive-invitation",
headers=headers,
json=data,
params=params,
)
print(f"response = {response}")
json = response.json()
print(f"receive_invitation json = {json}")
connection_id = json["connection_id"]
return connection_id
def fetch_connection(connection_id, url, headers=None):
print("\nfetch_connection")
if headers is None:
headers = {}
response = requests.get(
f"{url}/connections/{connection_id}",
headers=headers,
)
print(f"response = {response}")
json = response.json()
print(f"fetch_connection json = {json}")
return json["state"] == "active"
def ping_connection(connection_id, alias, url, headers=None):
if headers is None:
headers = {}
response = requests.post(
f"{url}/connections/{connection_id}/send-ping",
headers=headers,
json={"comment": f"{alias} pinging..."},
)
print(f"ping_connection = {response}")
def create_tenant(wallet_name, wallet_key, url, headers=None):
print("\ncreate_tenant")
if headers is None:
headers = {}
# need to create the tenant and get the token
data = {
"key_management_mode": "managed",
"wallet_dispatch_type": "default",
"wallet_name": wallet_name,
"wallet_key": wallet_key,
"label": wallet_name,
"wallet_type": "askar",
"wallet_webhook_urls": [],
}
# multi-agent has no security for base wallet, just call multitenancy/wallet
# to create a new tenant
response = requests.post(f"{url}/multitenancy/wallet", headers=headers, json=data)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
wallet_id = json["wallet_id"]
token = json["token"]
_headers = {"Authorization": f"Bearer {token}"}
return wallet_id, token, _headers
def create_public_did(alias, url, headers=None):
print("\ncreate_public_did")
if headers is None:
headers = {}
response = requests.post(f"{url}/wallet/did/create", headers=headers)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
did = json["result"]["did"]
verkey = json["result"]["verkey"]
data = {"alias": alias, "did": did, "verkey": verkey, "role": "TRUST_ANCHOR"}
response = requests.post(f"{LEDGER_URL}/register", json=data)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
wait_a_bit(1)
response = requests.post(f"{url}/wallet/did/public?did={did}", headers=headers)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
wait_a_bit(1)
response = requests.get(f"{url}/wallet/did/public", headers=headers)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
return did
def create_schema(data, url, headers=None):
print("\ncreate_schema")
if headers is None:
headers = {}
if data is None:
data = {
"schema_name": "schema_name",
"schema_version": "1.0",
"attributes": ["name", "age"],
}
response = requests.post(f"{url}/schemas", headers=headers, json=data)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
schema_id = json["schema_id"]
return schema_id
def create_cred_def(schema_id, data, url, headers=None):
print("\ncreate_cred_def")
if headers is None:
headers = {}
if data is None:
data = {
"schema_id": schema_id,
"support_revocation": False,
"tag": str(uuid.uuid4())[0:8],
}
response = requests.post(
f"{url}/credential-definitions", headers=headers, json=data
)
print(f"response = {response}")
json = response.json()
print(f"json = {json}")
cred_def_id = json["credential_definition_id"]
return cred_def_id
def offer_credential(connection_id, cred_def_id, data, url, headers=None):
print("\noffer_credential")
if headers is None:
headers = {}
if data is None:
cred_preview = {
"@type": "https://didcomm.org/issue-credential/2.0/credential-preview",
"attributes": [
{"name": "name", "value": "Joe"},
{"name": "age", "value": "30"},
],
}
data = {
"connection_id": connection_id,
"comment": f"Offer on cred def id {cred_def_id}",
"auto_remove": False,
"credential_preview": cred_preview,
"filter": {"indy": {"cred_def_id": cred_def_id}},
}
response = requests.post(
f"{url}/issue-credential-2.0/send-offer", headers=headers, json=data
)
print(f"response = {response}")
json = response.json()
# print(f"json = {json}")
return json
def list_issue_credential_records(url, headers=None):
print("\nlist_issue_credential_records")
if headers is None:
headers = {}
response = requests.get(f"{url}/issue-credential-2.0/records", headers=headers)
print(f"response = {response}")
if response.ok:
json = response.json()
# print(f"json = {json}")
for r in json["results"]:
print(
f"cred_ex_id = {r['cred_ex_record']['cred_ex_id']}, state = {r['cred_ex_record']['state']}"
)
return json
def get_issue_credential_record(cred_ex_id, url, headers=None):
print("\nget_issue_credential_record")
if headers is None:
headers = {}
response = requests.get(
f"{url}/issue-credential-2.0/records/{cred_ex_id}", headers=headers
)
print(f"response = {response}")
if response.ok:
json = response.json()
# print(f"json = {json}")
print(
f"cred_ex_id = {json['cred_ex_record']['cred_ex_id']}, state = {json['cred_ex_record']['state']}"
)
return json
def delete_issue_credential_record(cred_ex_id, url, headers=None):
print("\ndelete_issue_credential_record")
if headers is None:
headers = {}
response = requests.delete(
f"{url}/issue-credential-2.0/records/{cred_ex_id}", headers=headers
)
print(f"response = {response}")
json = response.json()
# print(f"json = {json}")
return json
def issue_credential_send_request(cred_ex_id, url, headers=None):
print("\nissue_credential_send_request")
if headers is None:
headers = {}
data = {"auto_remove": False}
response = requests.post(
f"{url}/issue-credential-2.0/records/{cred_ex_id}/send-request",
headers=headers,
json=data,
)
print(f"response = {response}")
json = response.json()
# print(f"json = {json}")
return json
def issue_credential(cred_ex_id, url, headers=None):
print("\issue_credential")
if headers is None:
headers = {}
data = {"comment": f"Issuing credential, exchange {cred_ex_id}"}
response = requests.post(
f"{url}/issue-credential-2.0/records/{cred_ex_id}/issue",
headers=headers,
json=data,
)
print(f"response = {response}")
json = response.json()
# print(f"json = {json}")
return json
def list_credentials(url, headers=None):
print("\list_credentials")
if headers is None:
headers = {}
response = requests.get(f"{url}/credentials", headers=headers)
print(f"response = {response}")
if response.ok:
json = response.json()
print(f"json = {json}")
# for r in json["results"]:
# print(
# f"cred_ex_id = {r['cred_ex_record']['cred_ex_id']}, state = {r['cred_ex_record']['state']}"
# )
return json
if __name__ == "__main__":
print("\n... multitenant create tenant(s)...\n")
# ok, now let's try with multitenant
issuer_wallet_name = f"issuer_{str(uuid.uuid4())[0:8]}"
issuer_wallet_id, issuer_token, issuer_headers = create_tenant(
issuer_wallet_name, "changeme", MULTI_ADMIN_URL
)
issuer_public_did = create_public_did(
issuer_wallet_name, MULTI_ADMIN_URL, issuer_headers
)
holder_wallet_name = f"holder_{str(uuid.uuid4())[0:8]}"
holder_wallet_id, holder_token, holder_headers = create_tenant(
holder_wallet_name, "changeme", MULTI_ADMIN_URL
)
holder_public_did = create_public_did(
holder_wallet_name, MULTI_ADMIN_URL, holder_headers
)
schema_id = create_schema(None, MULTI_ADMIN_URL, issuer_headers)
wait_a_bit(1)
cred_def_id = create_cred_def(schema_id, None, MULTI_ADMIN_URL, issuer_headers)
wait_a_bit(1)
cred_ex = create_offer(
cred_def_id,
None,
MULTI_ADMIN_URL,
issuer_headers,
)
issuer_cred_ex_id = cred_ex["cred_ex_id"]
oob_invitation, oob_id = create_oob_invitation_with_offer(
issuer_wallet_name,
holder_wallet_name,
issuer_cred_ex_id,
None,
MULTI_ADMIN_URL,
issuer_headers,
)
holder_to_issuer_connection_id = receive_oob_invitation(
oob_invitation,
issuer_wallet_name,
None,
MULTI_ADMIN_URL,
holder_headers,
)
wait_a_bit(30)
This is requesting support for a “connection-less issue”. That is not something that the BC Gov team is a big fan of, so we have not implemented this. We’d welcome someone implementing this (it’s come up before), but I don’t think anyone has as yet, so not surprising it is not working. It’s been implemented in other Aries implementations — at least .NET and I think AFJ — so it is definitely do-able.
I'm also having this problem. I tested using a multitenant acapy agent as holder, with wallet-type = askar-profile (so it uses a single database), and got the same result. If I use the default multitenant behavior (which creates a database per user), it doesn't throws an error, however the credential offer doesn't work. That was the error message in the log:
2024-05-09 13:25:30,485 aries_cloudagent.core.conductor ERROR Exception in message handler:
Traceback (most recent call last):
File "/usr/local/lib/python3.9/asyncio/tasks.py", line 256, in __step
result = coro.send(None)
File "/home/aries/.local/lib/python3.9/site-packages/aries_cloudagent/core/dispatcher.py", line 182, in handle_message
connection = await ConnRecord.retrieve_by_id(
File "/home/aries/.local/lib/python3.9/site-packages/aries_cloudagent/messaging/models/base_record.py", line 230, in retrieve_by_id
result = await storage.get_record(
File "/home/aries/.local/lib/python3.9/site-packages/aries_cloudagent/storage/askar.py", line 92, in get_record
raise StorageNotFoundError(f"Record not found: {record_type}/{record_id}")
aries_cloudagent.storage.error.StorageNotFoundError: Record not found: connection/8b6870ed-3d66-4828-b5b9-ca4598b65e0e
2024-05-09 13:25:30,486 aries_cloudagent.core.conductor ERROR DON'T shutdown on StorageNotFoundError Record not found: connection/8b6870ed-3d66-4828-b5b9-ca4598b65e0e
2024-05-09 13:25:30,486 aries_cloudagent.core.dispatcher ERROR Handler error: Dispatcher.handle_message
Traceback (most recent call last):
File "/usr/local/lib/python3.9/asyncio/tasks.py", line 256, in __step
result = coro.send(None)
File "/home/aries/.local/lib/python3.9/site-packages/aries_cloudagent/core/dispatcher.py", line 182, in handle_message
connection = await ConnRecord.retrieve_by_id(
File "/home/aries/.local/lib/python3.9/site-packages/aries_cloudagent/messaging/models/base_record.py", line 230, in retrieve_by_id
result = await storage.get_record(
File "/home/aries/.local/lib/python3.9/site-packages/aries_cloudagent/storage/askar.py", line 92, in get_record
raise StorageNotFoundError(f"Record not found: {record_type}/{record_id}")
aries_cloudagent.storage.error.StorageNotFoundError: Record not found: connection/8b6870ed-3d66-4828-b5b9-ca4598b65e0e
Using a single tenant acapy as holder works without any error.
I think this issue was resolved at least over then 0.12.1. I can able to attach credential-offer with multitenent and askar-profile wallet type.
@thiagoromanos have you tried version 0.12.1?
Since 0.12.1 can be able to do. I'll closing this issue.