aries-cloudagent-python icon indicating copy to clipboard operation
aries-cloudagent-python copied to clipboard

oob invitation with credential-offer attachment failed.

Open kukgini opened this issue 2 years ago • 5 comments
trafficstars

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:

  1. create credential-offer with API /issue-credential/create-offer
  2. create oob invitation with API /out-of-band/create-invitation?auto_accept=true
    • attachments: [ { type: "credential-offer", id: <cred_ex_id from step 1>} ]
  3. send oob invitation to holder side

holder side:

  1. 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

kukgini avatar Sep 06 '23 02:09 kukgini

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

kukgini avatar Sep 06 '23 23:09 kukgini

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)

usingtechnology avatar Dec 14 '23 00:12 usingtechnology

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.

swcurran avatar Dec 19 '23 00:12 swcurran

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.

thiagoromanos avatar May 09 '24 15:05 thiagoromanos

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?

kukgini avatar May 28 '24 01:05 kukgini

Since 0.12.1 can be able to do. I'll closing this issue.

kukgini avatar Jun 04 '24 00:06 kukgini