From efe14c73ed1fc5b73d962dbd13eada56cbd2b8fe Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Fri, 20 Dec 2024 09:51:54 -0800 Subject: [PATCH] Scenario test with anoncreds wallet upgrade and restart (#3410) * Scenario test with anoncreds wallet upgrade and restart Signed-off-by: Ian Costanzo * Added some meat to the anoncreds upgrade test Signed-off-by: Ian Costanzo * Fix anoncreds upgrade function Signed-off-by: Ian Costanzo --------- Signed-off-by: Ian Costanzo Co-authored-by: Stephen Curran --- acapy_agent/wallet/anoncreds_upgrade.py | 1 + .../example.py | 402 +---------------- .../docker-compose.yml | 208 +++++++++ .../restart_anoncreds_upgrade/example.py | 324 +++++++++++++ scenarios/examples/simple_restart/example.py | 75 ++- scenarios/examples/util.py | 427 ++++++++++++++++++ 6 files changed, 1011 insertions(+), 426 deletions(-) create mode 100644 scenarios/examples/restart_anoncreds_upgrade/docker-compose.yml create mode 100644 scenarios/examples/restart_anoncreds_upgrade/example.py create mode 100644 scenarios/examples/util.py diff --git a/acapy_agent/wallet/anoncreds_upgrade.py b/acapy_agent/wallet/anoncreds_upgrade.py index 4261acf0eb..3335c5ae29 100644 --- a/acapy_agent/wallet/anoncreds_upgrade.py +++ b/acapy_agent/wallet/anoncreds_upgrade.py @@ -472,6 +472,7 @@ async def get_rev_reg_def_upgrade_objs( key=lambda x: json.loads(x.value)["created_at"], ) found_active = False + is_active = False for askar_issuer_rev_reg_def in askar_issuer_rev_reg_def_records: # active rev reg def is the oldest non-full and active rev reg def if ( diff --git a/scenarios/examples/anoncreds_issuance_and_revocation/example.py b/scenarios/examples/anoncreds_issuance_and_revocation/example.py index 05a68c395a..45a4907f52 100644 --- a/scenarios/examples/anoncreds_issuance_and_revocation/example.py +++ b/scenarios/examples/anoncreds_issuance_and_revocation/example.py @@ -5,11 +5,9 @@ import asyncio import json -from dataclasses import dataclass from os import getenv -from secrets import randbelow, token_hex +from secrets import token_hex from typing import Any, Dict, List, Mapping, Optional, Tuple, Type, Union -from uuid import uuid4 from acapy_controller import Controller from acapy_controller.controller import Minimal, MinType @@ -28,377 +26,21 @@ ) from aiohttp import ClientSession +from examples.util import ( + SchemaResultAnoncreds, + CredDefResultAnoncreds, + anoncreds_presentation_summary, + auto_select_credentials_for_presentation_request, + anoncreds_issue_credential_v2, + anoncreds_present_proof_v2, +) + + AGENCY = getenv("AGENCY", "http://agency:3001") HOLDER_ANONCREDS = getenv("HOLDER_ANONCREDS", "http://holder_anoncreds:3001") HOLDER_INDY = getenv("HOLDER_INDY", "http://holder_indy:3001") -def summary(presentation: V20PresExRecord) -> str: - """Summarize a presentation exchange record.""" - request = presentation.pres_request - return "Summary: " + json.dumps( - { - "state": presentation.state, - "verified": presentation.verified, - "presentation_request": request.model_dump(by_alias=True) - if request - else None, - }, - indent=2, - sort_keys=True, - ) - - -@dataclass -class SchemaResultAnoncreds(Minimal): - """Schema result.""" - - schema_state: dict - - -@dataclass -class CredDefResultAnoncreds(Minimal): - """Credential definition result.""" - - credential_definition_state: dict - - -@dataclass -class V20CredExRecord(Minimal): - """V2.0 credential exchange record.""" - - state: str - cred_ex_id: str - connection_id: str - thread_id: str - - -@dataclass -class V20CredExRecordFormat(Minimal): - """V2.0 credential exchange record anoncreds.""" - - rev_reg_id: Optional[str] = None - cred_rev_id: Optional[str] = None - - -@dataclass -class V20CredExRecordDetail(Minimal): - """V2.0 credential exchange record detail.""" - - cred_ex_record: V20CredExRecord - details: Optional[V20CredExRecordFormat] = None - - -@dataclass -class ProofRequest(Minimal): - """Proof request.""" - - requested_attributes: Dict[str, Any] - requested_predicates: Dict[str, Any] - - -@dataclass -class PresSpec(Minimal): - """Presentation specification.""" - - requested_attributes: Dict[str, Any] - requested_predicates: Dict[str, Any] - self_attested_attributes: Dict[str, Any] - - -@dataclass -class CredInfo(Minimal): - """Credential information.""" - - referent: str - attrs: Dict[str, Any] - - -@dataclass -class CredPrecis(Minimal): - """Credential precis.""" - - cred_info: CredInfo - presentation_referents: List[str] - - @classmethod - def deserialize(cls: Type[MinType], value: Mapping[str, Any]) -> MinType: - """Deserialize the credential precis.""" - value = dict(value) - if cred_info := value.get("cred_info"): - value["cred_info"] = CredInfo.deserialize(cred_info) - return super().deserialize(value) - - -@dataclass -class Settings(Minimal): - """Settings information.""" - - -def auto_select_credentials_for_presentation_request( - presentation_request: Union[ProofRequest, dict], - relevant_creds: List[CredPrecis], -) -> PresSpec: - """Select credentials to use for presentation automatically.""" - if isinstance(presentation_request, dict): - presentation_request = ProofRequest.deserialize(presentation_request) - - requested_attributes = {} - for pres_referrent in presentation_request.requested_attributes.keys(): - for cred_precis in relevant_creds: - if pres_referrent in cred_precis.presentation_referents: - requested_attributes[pres_referrent] = { - "cred_id": cred_precis.cred_info.referent, - "revealed": True, - } - requested_predicates = {} - for pres_referrent in presentation_request.requested_predicates.keys(): - for cred_precis in relevant_creds: - if pres_referrent in cred_precis.presentation_referents: - requested_predicates[pres_referrent] = { - "cred_id": cred_precis.cred_info.referent, - } - - return PresSpec.deserialize( - { - "requested_attributes": requested_attributes, - "requested_predicates": requested_predicates, - "self_attested_attributes": {}, - } - ) - - -async def issue_credential_v2( - issuer: Controller, - holder: Controller, - issuer_connection_id: str, - holder_connection_id: str, - cred_def_id: str, - attributes: Mapping[str, str], -) -> Tuple[V20CredExRecordDetail, V20CredExRecordDetail]: - """Issue an credential using issue-credential/2.0. - - Issuer and holder should already be connected. - """ - - is_issuer_anoncreds = (await issuer.get("/settings", response=Settings)).get( - "wallet.type" - ) == "askar-anoncreds" - is_holder_anoncreds = (await holder.get("/settings", response=Settings)).get( - "wallet.type" - ) == "askar-anoncreds" - - if is_issuer_anoncreds: - _filter = {"anoncreds": {"cred_def_id": cred_def_id}} - else: - _filter = {"indy": {"cred_def_id": cred_def_id}} - issuer_cred_ex = await issuer.post( - "/issue-credential-2.0/send-offer", - json={ - "auto_issue": False, - "auto_remove": False, - "comment": "Credential from minimal example", - "trace": False, - "connection_id": issuer_connection_id, - "filter": _filter, - "credential_preview": { - "type": "issue-credential-2.0/2.0/credential-preview", # pyright: ignore - "attributes": [ - { - "mime_type": None, - "name": name, - "value": value, - } - for name, value in attributes.items() - ], - }, - }, - response=V20CredExRecord, - ) - issuer_cred_ex_id = issuer_cred_ex.cred_ex_id - - holder_cred_ex = await holder.event_with_values( - topic="issue_credential_v2_0", - event_type=V20CredExRecord, - connection_id=holder_connection_id, - state="offer-received", - ) - holder_cred_ex_id = holder_cred_ex.cred_ex_id - - await holder.post( - f"/issue-credential-2.0/records/{holder_cred_ex_id}/send-request", - response=V20CredExRecord, - ) - - await issuer.event_with_values( - topic="issue_credential_v2_0", - cred_ex_id=issuer_cred_ex_id, - state="request-received", - ) - - await issuer.post( - f"/issue-credential-2.0/records/{issuer_cred_ex_id}/issue", - json={}, - response=V20CredExRecordDetail, - ) - - await holder.event_with_values( - topic="issue_credential_v2_0", - cred_ex_id=holder_cred_ex_id, - state="credential-received", - ) - - await holder.post( - f"/issue-credential-2.0/records/{holder_cred_ex_id}/store", - json={}, - response=V20CredExRecordDetail, - ) - issuer_cred_ex = await issuer.event_with_values( - topic="issue_credential_v2_0", - event_type=V20CredExRecord, - cred_ex_id=issuer_cred_ex_id, - state="done", - ) - issuer_indy_record = await issuer.event_with_values( - topic="issue_credential_v2_0_anoncreds" - if is_issuer_anoncreds - else "issue_credential_v2_0_indy", - event_type=V20CredExRecordIndy, - ) - - holder_cred_ex = await holder.event_with_values( - topic="issue_credential_v2_0", - event_type=V20CredExRecord, - cred_ex_id=holder_cred_ex_id, - state="done", - ) - holder_indy_record = await holder.event_with_values( - topic="issue_credential_v2_0_anoncreds" - if is_holder_anoncreds - else "issue_credential_v2_0_indy", - event_type=V20CredExRecordIndy, - ) - - return ( - V20CredExRecordDetail(cred_ex_record=issuer_cred_ex, details=issuer_indy_record), - V20CredExRecordDetail( - cred_ex_record=holder_cred_ex, - details=holder_indy_record, - ), - ) - - -async def present_proof_v2( - holder: Controller, - verifier: Controller, - holder_connection_id: str, - verifier_connection_id: str, - *, - name: Optional[str] = None, - version: Optional[str] = None, - comment: Optional[str] = None, - requested_attributes: Optional[List[Mapping[str, Any]]] = None, - requested_predicates: Optional[List[Mapping[str, Any]]] = None, - non_revoked: Optional[Mapping[str, int]] = None, -): - """Present an credential using present proof v2.""" - - is_verifier_anoncreds = (await verifier.get("/settings", response=Settings)).get( - "wallet.type" - ) == "askar-anoncreds" - - attrs = { - "name": name or "proof", - "version": version or "0.1.0", - "nonce": str(randbelow(10**10)), - "requested_attributes": { - str(uuid4()): attr for attr in requested_attributes or [] - }, - "requested_predicates": { - str(uuid4()): pred for pred in requested_predicates or [] - }, - "non_revoked": (non_revoked if non_revoked else None), - } - - if is_verifier_anoncreds: - presentation_request = { - "anoncreds": attrs, - } - else: - presentation_request = { - "indy": attrs, - } - verifier_pres_ex = await verifier.post( - "/present-proof-2.0/send-request", - json={ - "auto_verify": False, - "comment": comment or "Presentation request from minimal", - "connection_id": verifier_connection_id, - "presentation_request": presentation_request, - "trace": False, - }, - response=V20PresExRecord, - ) - verifier_pres_ex_id = verifier_pres_ex.pres_ex_id - - holder_pres_ex = await holder.event_with_values( - topic="present_proof_v2_0", - event_type=V20PresExRecord, - connection_id=holder_connection_id, - state="request-received", - ) - assert holder_pres_ex.pres_request - holder_pres_ex_id = holder_pres_ex.pres_ex_id - - relevant_creds = await holder.get( - f"/present-proof-2.0/records/{holder_pres_ex_id}/credentials", - response=List[CredPrecis], - ) - assert holder_pres_ex.by_format.pres_request - proof_request = holder_pres_ex.by_format.pres_request.get( - "anoncreds" - ) or holder_pres_ex.by_format.pres_request.get("indy") - pres_spec = auto_select_credentials_for_presentation_request( - proof_request, relevant_creds - ) - if is_verifier_anoncreds: - proof = {"anoncreds": pres_spec.serialize()} - else: - proof = {"indy": pres_spec.serialize()} - await holder.post( - f"/present-proof-2.0/records/{holder_pres_ex_id}/send-presentation", - json=proof, - response=V20PresExRecord, - ) - - await verifier.event_with_values( - topic="present_proof_v2_0", - event_type=V20PresExRecord, - pres_ex_id=verifier_pres_ex_id, - state="presentation-received", - ) - await verifier.post( - f"/present-proof-2.0/records/{verifier_pres_ex_id}/verify-presentation", - json={}, - response=V20PresExRecord, - ) - verifier_pres_ex = await verifier.event_with_values( - topic="present_proof_v2_0", - event_type=V20PresExRecord, - pres_ex_id=verifier_pres_ex_id, - state="done", - ) - - holder_pres_ex = await holder.event_with_values( - topic="present_proof_v2_0", - event_type=V20PresExRecord, - pres_ex_id=holder_pres_ex_id, - state="done", - ) - - return holder_pres_ex, verifier_pres_ex - - async def main(): """Test Controller protocols.""" issuer_name = "issuer" + token_hex(8) @@ -468,7 +110,7 @@ async def main(): ) # Issue a credential - issuer_cred_ex, _ = await issue_credential_v2( + issuer_cred_ex, _ = await anoncreds_issue_credential_v2( issuer, holder_anoncreds, issuer_conn_with_anoncreds_holder.connection_id, @@ -478,7 +120,7 @@ async def main(): ) # Present the the credential's attributes - await present_proof_v2( + await anoncreds_present_proof_v2( holder_anoncreds, issuer, holder_anoncreds_conn.connection_id, @@ -513,7 +155,7 @@ async def main(): ) # Issue a credential - issuer_cred_ex, _ = await issue_credential_v2( + issuer_cred_ex, _ = await anoncreds_issue_credential_v2( issuer, holder_indy, issuer_conn_with_indy_holder.connection_id, @@ -523,7 +165,7 @@ async def main(): ) # Present the the credential's attributes - await present_proof_v2( + await anoncreds_present_proof_v2( holder_indy, issuer, holder_indy_conn.connection_id, @@ -538,7 +180,7 @@ async def main(): # Presentation summary for _, pres in enumerate(presentations.results): - print(summary(pres)) + print(anoncreds_presentation_summary(pres)) # Revoke credential await issuer.post( @@ -575,7 +217,7 @@ async def main(): an anoncreds capable wallet (wallet type askar-anoncreds). """ # Presentation for anoncreds capable holder on existing credential - await present_proof_v2( + await anoncreds_present_proof_v2( holder_anoncreds, issuer, holder_anoncreds_conn.connection_id, @@ -584,7 +226,7 @@ async def main(): ) # Presentation for indy capable holder on existing credential - await present_proof_v2( + await anoncreds_present_proof_v2( holder_indy, issuer, holder_indy_conn.connection_id, @@ -620,7 +262,7 @@ async def main(): ) # Issue a new credential to anoncreds holder - issuer_cred_ex, _ = await issue_credential_v2( + issuer_cred_ex, _ = await anoncreds_issue_credential_v2( issuer, holder_anoncreds, issuer_conn_with_anoncreds_holder.connection_id, @@ -629,7 +271,7 @@ async def main(): {"middlename": "Anoncreds"}, ) # Presentation for anoncreds capable holder - await present_proof_v2( + await anoncreds_present_proof_v2( holder_anoncreds, issuer, holder_anoncreds_conn.connection_id, @@ -651,7 +293,7 @@ async def main(): await holder_anoncreds.record(topic="revocation-notification") # Issue a new credential to indy holder - issuer_cred_ex, _ = await issue_credential_v2( + issuer_cred_ex, _ = await anoncreds_issue_credential_v2( issuer, holder_indy, issuer_conn_with_indy_holder.connection_id, @@ -660,7 +302,7 @@ async def main(): {"middlename": "Indy"}, ) # Presentation for indy holder - await present_proof_v2( + await anoncreds_present_proof_v2( holder_indy, issuer, holder_indy_conn.connection_id, diff --git a/scenarios/examples/restart_anoncreds_upgrade/docker-compose.yml b/scenarios/examples/restart_anoncreds_upgrade/docker-compose.yml new file mode 100644 index 0000000000..caddadc8be --- /dev/null +++ b/scenarios/examples/restart_anoncreds_upgrade/docker-compose.yml @@ -0,0 +1,208 @@ +services: + wallet-db: + image: postgres:12 + environment: + - POSTGRES_USER=DB_USER + - POSTGRES_PASSWORD=DB_PASSWORD + ports: + - 5433:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U DB_USER"] + interval: 10s + retries: 5 + start_period: 30s + timeout: 10s + + alice: + image: acapy-test + ports: + - "3001:3001" + environment: + RUST_LOG: 'aries-askar::log::target=error' + command: > + start + --label Alice + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://alice:3000 + --admin 0.0.0.0 3001 + --admin-insecure-mode + --tails-server-base-url http://tails:6543 + --genesis-url http://test.bcovrin.vonx.io/genesis + --wallet-type askar + --wallet-name alice + --wallet-key insecure + --wallet-storage-type "postgres_storage" + --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" + --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" + --auto-provision + --log-level debug + --debug-webhooks + --notify-revocation + --preserve-exchange-records + healthcheck: + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null + start_period: 30s + interval: 7s + timeout: 5s + retries: 5 + depends_on: + tails: + condition: service_started + wallet-db: + condition: service_healthy + + bob-askar: + image: acapy-test + ports: + - "3002:3001" + environment: + RUST_LOG: 'aries-askar::log::target=error' + command: > + start + --label bob-askar + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://bob-askar:3000 + --admin 0.0.0.0 3001 + --admin-insecure-mode + --tails-server-base-url http://tails:6543 + --genesis-url http://test.bcovrin.vonx.io/genesis + --wallet-type askar + --wallet-name bob-askar + --wallet-key insecure + --wallet-storage-type "postgres_storage" + --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" + --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" + --auto-provision + --log-level debug + --debug-webhooks + --monitor-revocation-notification + --preserve-exchange-records + healthcheck: + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null + start_period: 30s + interval: 7s + timeout: 5s + retries: 5 + depends_on: + tails: + condition: service_started + wallet-db: + condition: service_healthy + + bob-anoncreds: + image: acapy-test + ports: + - "3003:3001" + environment: + RUST_LOG: 'aries-askar::log::target=error' + command: > + start + --label bob-anoncreds + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://bob-anoncreds:3000 + --admin 0.0.0.0 3001 + --admin-insecure-mode + --tails-server-base-url http://tails:6543 + --genesis-url http://test.bcovrin.vonx.io/genesis + --wallet-type askar-anoncreds + --wallet-name bob-anoncreds + --wallet-key insecure + --wallet-storage-type "postgres_storage" + --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" + --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" + --auto-provision + --log-level debug + --debug-webhooks + --monitor-revocation-notification + --preserve-exchange-records + healthcheck: + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null + start_period: 30s + interval: 7s + timeout: 5s + retries: 5 + depends_on: + tails: + condition: service_started + wallet-db: + condition: service_healthy + + bob-askar-anon: + image: acapy-test + ports: + - "3004:3001" + environment: + RUST_LOG: 'aries-askar::log::target=error' + command: > + start + --label bob-askar-anon + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://bob-askar-anon:3000 + --admin 0.0.0.0 3001 + --admin-insecure-mode + --tails-server-base-url http://tails:6543 + --genesis-url http://test.bcovrin.vonx.io/genesis + --wallet-type askar + --wallet-name bob-askar-anon + --wallet-key insecure + --wallet-storage-type "postgres_storage" + --wallet-storage-config "{\"url\":\"wallet-db:5432\",\"max_connections\":5}" + --wallet-storage-creds "{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"DB_USER\",\"admin_password\":\"DB_PASSWORD\"}" + --auto-provision + --log-level debug + --debug-webhooks + --monitor-revocation-notification + --preserve-exchange-records + healthcheck: + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null + start_period: 30s + interval: 7s + timeout: 5s + retries: 5 + depends_on: + tails: + condition: service_started + wallet-db: + condition: service_healthy + + tails: + image: ghcr.io/bcgov/tails-server:latest + ports: + - 6543:6543 + environment: + - GENESIS_URL=http://test.bcovrin.vonx.io/genesis + command: > + tails-server + --host 0.0.0.0 + --port 6543 + --storage-path /tmp/tails-files + --log-level INFO + + example: + container_name: controller + privileged: true + build: + context: ../.. + environment: + - DOCKER_HOST=unix:///var/run/docker.sock + - ALICE=http://alice:3001 + - BOB_ASKAR=http://bob-askar:3001 + - BOB_ANONCREDS=http://bob-anoncreds:3001 + - BOB_ASKAR_ANON=http://bob-askar-anon:3001 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./example.py:/usr/src/app/example.py:ro,z + command: python -m example + depends_on: + alice: + condition: service_healthy + bob-askar: + condition: service_healthy + bob-anoncreds: + condition: service_healthy + bob-askar-anon: + condition: service_healthy diff --git a/scenarios/examples/restart_anoncreds_upgrade/example.py b/scenarios/examples/restart_anoncreds_upgrade/example.py new file mode 100644 index 0000000000..5077090f04 --- /dev/null +++ b/scenarios/examples/restart_anoncreds_upgrade/example.py @@ -0,0 +1,324 @@ +"""Minimal reproducible example script. + +This script is for you to use to reproduce a bug or demonstrate a feature. +""" + +import asyncio +from os import getenv +import json +import time + +import docker +from docker.errors import NotFound +from docker.models.containers import Container + +from acapy_controller import Controller +from acapy_controller.logging import logging_to_stdout +from acapy_controller.protocols import ( + connection, + didexchange, + indy_anoncred_credential_artifacts, + indy_anoncred_onboard, + indy_anoncreds_publish_revocation, + indy_anoncreds_revoke, + indy_issue_credential_v2, + indy_present_proof_v2, +) + +from examples.util import ( + healthy, + unhealthy, + wait_until_healthy, + update_wallet_type, + get_wallet_name, + anoncreds_presentation_summary, + auto_select_credentials_for_presentation_request, + anoncreds_issue_credential_v2, + anoncreds_present_proof_v2, +) + + +ALICE = getenv("ALICE", "http://alice:3001") +BOB_ASKAR = getenv("BOB_ASKAR", "http://bob-askar:3001") +BOB_ANONCREDS = getenv("BOB_ANONCREDS", "http://bob-anoncreds:3001") +BOB_ASKAR_ANON = getenv("BOB_ASKAR_ANON", "http://bob-askar-anon:3001") + + +async def connect_agents_and_issue_credentials( + inviter: Controller, + invitee: Controller, + inviter_cred_def, + fname: str, + lname: str, +): + # connect the 2 agents + print(">>> connecting agents ...") + (inviter_conn, invitee_conn) = await didexchange(inviter, invitee) + + # Issue a credential + print(">>> issue credential ...") + inviter_cred_ex, _ = await anoncreds_issue_credential_v2( + inviter, + invitee, + inviter_conn.connection_id, + invitee_conn.connection_id, + inviter_cred_def.credential_definition_id, + {"firstname": fname, "lastname": lname}, + ) + print(">>> cred_ex:", inviter_cred_ex) + + # Present the the credential's attributes + print(">>> present proof ...") + await anoncreds_present_proof_v2( + invitee, + inviter, + invitee_conn.connection_id, + inviter_conn.connection_id, + requested_attributes=[{"name": "firstname"}], + ) + + # Revoke credential + await inviter.post( + url="/revocation/revoke", + json={ + "connection_id": inviter_conn.connection_id, + "rev_reg_id": inviter_cred_ex.details.rev_reg_id, + "cred_rev_id": inviter_cred_ex.details.cred_rev_id, + "publish": True, + "notify": True, + "notify_version": "v1_0", + }, + ) + await invitee.record(topic="revocation-notification") + + # Issue a second credential + print(">>> issue credential ...") + inviter_cred_ex, _ = await anoncreds_issue_credential_v2( + inviter, + invitee, + inviter_conn.connection_id, + invitee_conn.connection_id, + inviter_cred_def.credential_definition_id, + {"firstname": "{fname}2", "lastname": "{lname}2"}, + ) + print(">>> Done!") + + return (inviter_conn, invitee_conn) + + +async def upgrade_wallet_and_shutdown_container( + client, + agent_controller, + agent_container, +): + agent_command = agent_container.attrs['Config']['Cmd'] + + # command is a List, find the wallet type and replace "askar" with "askar-anoncreds" + correct_wallet_type = update_wallet_type(agent_command, "askar-anoncreds") + wallet_name = get_wallet_name(agent_command) + + # call the wallet upgrade endpoint to upgrade to askar-anoncreds + await agent_controller.post( + "/anoncreds/wallet/upgrade", + params={ + "wallet_name": wallet_name, + }, + ) + + # Wait for the upgrade ... + await asyncio.sleep(2) + + print(">>> waiting for container to exit ...") + agent_id = agent_container.attrs['Id'] + wait_until_healthy(client, agent_id, is_healthy=False) + agent_container.remove() + + return agent_command + + +def start_new_container( + client, + agent_command, + agent_container, + agent_label, +): + print(">>> start new container ...") + new_agent_container = client.containers.run( + 'acapy-test', + command=agent_command, + detach=True, + environment={'RUST_LOG': 'aries-askar::log::target=error'}, + healthcheck=agent_container.attrs['Config']['Healthcheck'], + name=agent_label, + network=agent_container.attrs['HostConfig']['NetworkMode'], + ports=agent_container.attrs['NetworkSettings']['Ports'], + ) + print(">>> new container:", agent_label, json.dumps(new_agent_container.attrs)) + new_agent_id = new_agent_container.attrs['Id'] + + wait_until_healthy(client, new_agent_id) + print(">>> new container is healthy") + + return (new_agent_container, new_agent_id) + + +def stop_and_remove_container(client, agent_id): + # cleanup - shut down agent (not part of docker compose) + print(">>> shut down agent ...") + agent_container = client.containers.get(agent_id) + agent_container.stop() + wait_until_healthy(client, agent_id, is_healthy=False) + agent_container.remove() + + +async def main(): + """Test Controller protocols.""" + async with Controller(base_url=ALICE) as alice: + # setup alice as an issuer + print(">>> setting up alice as issuer ...") + await indy_anoncred_onboard(alice) + schema, cred_def = await indy_anoncred_credential_artifacts( + alice, + ["firstname", "lastname"], + support_revocation=True, + revocation_registry_size=5, + ) + + alice_conns = {} + bob_conns = {} + async with Controller(base_url=ALICE) as alice, Controller(base_url=BOB_ASKAR) as bob: + # connect to Bob (Askar wallet) and issue (and revoke) some credentials + (alice_conn, bob_conn) = await connect_agents_and_issue_credentials( + alice, + bob, + cred_def, + "Bob", + "Askar", + ) + alice_conns["askar"] = alice_conn + bob_conns["askar"] = bob_conn + + async with Controller(base_url=ALICE) as alice, Controller(base_url=BOB_ANONCREDS) as bob: + # connect to Bob (Anoncreds wallet) and issue (and revoke) some credentials + (alice_conn, bob_conn) = await connect_agents_and_issue_credentials( + alice, + bob, + cred_def, + "Bob", + "Anoncreds", + ) + alice_conns["anoncreds"] = alice_conn + bob_conns["anoncreds"] = bob_conn + + async with Controller(base_url=ALICE) as alice, Controller(base_url=BOB_ASKAR_ANON) as bob: + # connect to Bob (Askar wallet which will be upgraded) and issue (and revoke) some credentials + (alice_conn, bob_conn) = await connect_agents_and_issue_credentials( + alice, + bob, + cred_def, + "Bob", + "Askar_Anon", + ) + alice_conns["askar-anon"] = alice_conn + bob_conns["askar-anon"] = bob_conn + + # at this point alice has issued 6 credentials (revocation registry size is 5) and revoked 3 + + # play with docker - get a list of all our running containers + client = docker.from_env() + containers = client.containers.list(all=True) + docker_containers = {} + for container in containers: + if 'com.docker.compose.service' in container.attrs['Config']['Labels']: + container_name = container.attrs['Config']['Labels']['com.docker.compose.service'] + container_id = container.attrs['Id'] + container_is_running = container.attrs['State']['Running'] + docker_containers[container_name] = {'Id': container_id, 'Running': container_is_running} + print(">>> container:", container_name, docker_containers[container_name]) + + alice_docker_container = docker_containers['alice'] + alice_container = client.containers.get(alice_docker_container['Id']) + async with Controller(base_url=ALICE) as alice: + alice_command = await upgrade_wallet_and_shutdown_container( + client, + alice, + alice_container, + ) + + bob_docker_container = docker_containers['bob-askar-anon'] + bob_container = client.containers.get(bob_docker_container['Id']) + async with Controller(base_url=BOB_ASKAR_ANON) as bob: + bob_command = await upgrade_wallet_and_shutdown_container( + client, + bob, + bob_container, + ) + + new_alice_container = None + alice_id = None + new_bob_container = None + bob_id = None + try: + (new_alice_container, alice_id) = start_new_container( + client, + alice_command, + alice_container, + "alice", + ) + + (new_bob_container, bob_id) = start_new_container( + client, + bob_command, + bob_container, + "bob-askar-anon", + ) + + # run some more tests ... alice should still be connected to bob for example ... + async with Controller(base_url=ALICE) as alice, Controller(base_url=BOB_ASKAR) as bob: + # Present the the credential's attributes + print(">>> present proof ... again ...") + await anoncreds_present_proof_v2( + bob, + alice, + bob_conns["askar"].connection_id, + alice_conns["askar"].connection_id, + requested_attributes=[{"name": "firstname"}], + ) + print(">>> Done! (again)") + + async with Controller(base_url=ALICE) as alice, Controller(base_url=BOB_ANONCREDS) as bob: + # Present the the credential's attributes + print(">>> present proof ... again ...") + await anoncreds_present_proof_v2( + bob, + alice, + bob_conns["anoncreds"].connection_id, + alice_conns["anoncreds"].connection_id, + requested_attributes=[{"name": "firstname"}], + ) + print(">>> Done! (again)") + + async with Controller(base_url=ALICE) as alice, Controller(base_url=BOB_ASKAR_ANON) as bob: + # Present the the credential's attributes + print(">>> present proof ... again ...") + await anoncreds_present_proof_v2( + bob, + alice, + bob_conns["askar-anon"].connection_id, + alice_conns["askar-anon"].connection_id, + requested_attributes=[{"name": "firstname"}], + ) + print(">>> Done! (again)") + + finally: + if alice_id and new_alice_container: + # cleanup - shut down alice agent (not part of docker compose) + stop_and_remove_container(client, alice_id) + if bob_id and new_bob_container: + # cleanup - shut down bob agent (not part of docker compose) + stop_and_remove_container(client, bob_id) + + +if __name__ == "__main__": + logging_to_stdout() + asyncio.run(main()) diff --git a/scenarios/examples/simple_restart/example.py b/scenarios/examples/simple_restart/example.py index 558d94285b..7737842360 100644 --- a/scenarios/examples/simple_restart/example.py +++ b/scenarios/examples/simple_restart/example.py @@ -11,7 +11,6 @@ import docker from docker.errors import NotFound from docker.models.containers import Container -from docker.models.networks import Network from acapy_controller import Controller from acapy_controller.logging import logging_to_stdout @@ -26,33 +25,15 @@ indy_present_proof_v2, ) -ALICE = getenv("ALICE", "http://alice:3001") -BOB = getenv("BOB", "http://bob:3001") - - -def healthy(container: Container) -> bool: - """Check if container is healthy.""" - inspect_results = container.attrs - return inspect_results["State"]["Running"] and inspect_results["State"]["Health"]["Status"] == "healthy" - - -def unhealthy(container: Container) -> bool: - """Check if container is unhealthy.""" - inspect_results = container.attrs - return not inspect_results["State"]["Running"] +from examples.util import ( + healthy, + unhealthy, + wait_until_healthy, +) -def wait_until_healthy(client, container_id: str, attempts: int = 350, is_healthy=True): - """Wait until container is healthy.""" - container = client.containers.get(container_id) - print((container.name, container.status)) - for _ in range(attempts): - if (is_healthy and healthy(container)) or unhealthy(container): - return - else: - time.sleep(1) - container = client.containers.get(container_id) - raise TimeoutError("Timed out waiting for container") +ALICE = getenv("ALICE", "http://alice:3001") +BOB = getenv("BOB", "http://bob:3001") async def main(): @@ -117,21 +98,22 @@ async def main(): wait_until_healthy(client, alice_id, is_healthy=False) alice_container.remove() - print(">>> start new alice container ...") - new_alice_container = client.containers.run( - 'acapy-test', - command=alice_container.attrs['Config']['Cmd'], - detach=True, - environment={'RUST_LOG': 'aries-askar::log::target=error'}, - healthcheck=alice_container.attrs['Config']['Healthcheck'], - name='alice', - network=alice_container.attrs['HostConfig']['NetworkMode'], - ports=alice_container.attrs['NetworkSettings']['Ports'], - ) - print(">>> new container:", 'alice', json.dumps(new_alice_container.attrs)) - alice_id = new_alice_container.attrs['Id'] - + new_alice_container = None + alice_id = None try: + print(">>> start new alice container ...") + new_alice_container = client.containers.run( + 'acapy-test', + command=alice_container.attrs['Config']['Cmd'], + detach=True, + environment={'RUST_LOG': 'aries-askar::log::target=error'}, + healthcheck=alice_container.attrs['Config']['Healthcheck'], + name='alice', + network=alice_container.attrs['HostConfig']['NetworkMode'], + ports=alice_container.attrs['NetworkSettings']['Ports'], + ) + alice_id = new_alice_container.attrs['Id'] + wait_until_healthy(client, alice_id) print(">>> new alice container is healthy") @@ -148,12 +130,13 @@ async def main(): ) print(">>> Done! (again)") finally: - # cleanup - shut down alice agent (not part of docker compose) - print(">>> shut down alice ...") - alice_container = client.containers.get(alice_id) - alice_container.stop() - wait_until_healthy(client, alice_id, is_healthy=False) - alice_container.remove() + if alice_id and new_alice_container: + # cleanup - shut down alice agent (not part of docker compose) + print(">>> shut down alice ...") + alice_container = client.containers.get(alice_id) + alice_container.stop() + wait_until_healthy(client, alice_id, is_healthy=False) + alice_container.remove() if __name__ == "__main__": diff --git a/scenarios/examples/util.py b/scenarios/examples/util.py new file mode 100644 index 0000000000..5e033905af --- /dev/null +++ b/scenarios/examples/util.py @@ -0,0 +1,427 @@ +import json +from dataclasses import dataclass +from secrets import randbelow +from typing import Any, Dict, List, Mapping, Optional, Tuple, Type, Union +from uuid import uuid4 +import time + +import docker +from docker.models.containers import Container + +from acapy_controller import Controller +from acapy_controller.controller import Minimal, MinType +from acapy_controller.logging import logging_to_stdout +from acapy_controller.models import ( + CreateWalletResponse, + V20CredExRecordIndy, + V20PresExRecord, + V20PresExRecordList, +) + + +# docker utilities: +def healthy(container: Container) -> bool: + """Check if container is healthy.""" + inspect_results = container.attrs + return inspect_results["State"]["Running"] and inspect_results["State"]["Health"]["Status"] == "healthy" + + +def unhealthy(container: Container) -> bool: + """Check if container is unhealthy.""" + inspect_results = container.attrs + return not inspect_results["State"]["Running"] + + +def wait_until_healthy(client, container_id: str, attempts: int = 350, is_healthy=True): + """Wait until container is healthy.""" + container = client.containers.get(container_id) + print((container.name, container.status)) + for _ in range(attempts): + if (is_healthy and healthy(container)) or unhealthy(container): + return + else: + time.sleep(1) + container = client.containers.get(container_id) + raise TimeoutError("Timed out waiting for container") + + +def update_wallet_type(agent_command: List, wallet_type:str) -> str: + for i in range(len(agent_command)-1): + if agent_command[i] == "--wallet-type": + agent_command[i+1] = wallet_type + return wallet_type + raise Exception("Error unable to upgrade wallet type to askar-anoncreds") + + +def get_wallet_name(agent_command: List) -> str: + for i in range(len(agent_command)-1): + if agent_command[i] == "--wallet-name": + return agent_command[i+1] + raise Exception("Error unable to upgrade wallet type to askar-anoncreds") + + +# anoncreds utilities: +def anoncreds_presentation_summary(presentation: V20PresExRecord) -> str: + """Summarize a presentation exchange record.""" + request = presentation.pres_request + return "Summary: " + json.dumps( + { + "state": presentation.state, + "verified": presentation.verified, + "presentation_request": request.model_dump(by_alias=True) + if request + else None, + }, + indent=2, + sort_keys=True, + ) + + +@dataclass +class SchemaResultAnoncreds(Minimal): + """Schema result.""" + + schema_state: dict + + +@dataclass +class CredDefResultAnoncreds(Minimal): + """Credential definition result.""" + + credential_definition_state: dict + + +@dataclass +class V20CredExRecord(Minimal): + """V2.0 credential exchange record.""" + + state: str + cred_ex_id: str + connection_id: str + thread_id: str + + +@dataclass +class V20CredExRecordFormat(Minimal): + """V2.0 credential exchange record anoncreds.""" + + rev_reg_id: Optional[str] = None + cred_rev_id: Optional[str] = None + + +@dataclass +class V20CredExRecordDetail(Minimal): + """V2.0 credential exchange record detail.""" + + cred_ex_record: V20CredExRecord + details: Optional[V20CredExRecordFormat] = None + + +@dataclass +class ProofRequest(Minimal): + """Proof request.""" + + requested_attributes: Dict[str, Any] + requested_predicates: Dict[str, Any] + + +@dataclass +class PresSpec(Minimal): + """Presentation specification.""" + + requested_attributes: Dict[str, Any] + requested_predicates: Dict[str, Any] + self_attested_attributes: Dict[str, Any] + + +@dataclass +class CredInfo(Minimal): + """Credential information.""" + + referent: str + attrs: Dict[str, Any] + + +@dataclass +class CredPrecis(Minimal): + """Credential precis.""" + + cred_info: CredInfo + presentation_referents: List[str] + + @classmethod + def deserialize(cls: Type[MinType], value: Mapping[str, Any]) -> MinType: + """Deserialize the credential precis.""" + value = dict(value) + if cred_info := value.get("cred_info"): + value["cred_info"] = CredInfo.deserialize(cred_info) + return super().deserialize(value) + + +@dataclass +class Settings(Minimal): + """Settings information.""" + + +def auto_select_credentials_for_presentation_request( + presentation_request: Union[ProofRequest, dict], + relevant_creds: List[CredPrecis], +) -> PresSpec: + """Select credentials to use for presentation automatically.""" + if isinstance(presentation_request, dict): + presentation_request = ProofRequest.deserialize(presentation_request) + + requested_attributes = {} + for pres_referrent in presentation_request.requested_attributes.keys(): + for cred_precis in relevant_creds: + if pres_referrent in cred_precis.presentation_referents: + requested_attributes[pres_referrent] = { + "cred_id": cred_precis.cred_info.referent, + "revealed": True, + } + requested_predicates = {} + for pres_referrent in presentation_request.requested_predicates.keys(): + for cred_precis in relevant_creds: + if pres_referrent in cred_precis.presentation_referents: + requested_predicates[pres_referrent] = { + "cred_id": cred_precis.cred_info.referent, + } + + return PresSpec.deserialize( + { + "requested_attributes": requested_attributes, + "requested_predicates": requested_predicates, + "self_attested_attributes": {}, + } + ) + + +async def anoncreds_issue_credential_v2( + issuer: Controller, + holder: Controller, + issuer_connection_id: str, + holder_connection_id: str, + cred_def_id: str, + attributes: Mapping[str, str], +) -> Tuple[V20CredExRecordDetail, V20CredExRecordDetail]: + """Issue an credential using issue-credential/2.0. + + Issuer and holder should already be connected. + """ + + is_issuer_anoncreds = (await issuer.get("/settings", response=Settings)).get( + "wallet.type" + ) == "askar-anoncreds" + is_holder_anoncreds = (await holder.get("/settings", response=Settings)).get( + "wallet.type" + ) == "askar-anoncreds" + + if is_issuer_anoncreds: + _filter = {"anoncreds": {"cred_def_id": cred_def_id}} + else: + _filter = {"indy": {"cred_def_id": cred_def_id}} + issuer_cred_ex = await issuer.post( + "/issue-credential-2.0/send-offer", + json={ + "auto_issue": False, + "auto_remove": False, + "comment": "Credential from minimal example", + "trace": False, + "connection_id": issuer_connection_id, + "filter": _filter, + "credential_preview": { + "type": "issue-credential-2.0/2.0/credential-preview", # pyright: ignore + "attributes": [ + { + "mime_type": None, + "name": name, + "value": value, + } + for name, value in attributes.items() + ], + }, + }, + response=V20CredExRecord, + ) + issuer_cred_ex_id = issuer_cred_ex.cred_ex_id + + holder_cred_ex = await holder.event_with_values( + topic="issue_credential_v2_0", + event_type=V20CredExRecord, + connection_id=holder_connection_id, + state="offer-received", + ) + holder_cred_ex_id = holder_cred_ex.cred_ex_id + + await holder.post( + f"/issue-credential-2.0/records/{holder_cred_ex_id}/send-request", + response=V20CredExRecord, + ) + + await issuer.event_with_values( + topic="issue_credential_v2_0", + cred_ex_id=issuer_cred_ex_id, + state="request-received", + ) + + await issuer.post( + f"/issue-credential-2.0/records/{issuer_cred_ex_id}/issue", + json={}, + response=V20CredExRecordDetail, + ) + + await holder.event_with_values( + topic="issue_credential_v2_0", + cred_ex_id=holder_cred_ex_id, + state="credential-received", + ) + + await holder.post( + f"/issue-credential-2.0/records/{holder_cred_ex_id}/store", + json={}, + response=V20CredExRecordDetail, + ) + issuer_cred_ex = await issuer.event_with_values( + topic="issue_credential_v2_0", + event_type=V20CredExRecord, + cred_ex_id=issuer_cred_ex_id, + state="done", + ) + issuer_indy_record = await issuer.event_with_values( + topic="issue_credential_v2_0_anoncreds" + if is_issuer_anoncreds + else "issue_credential_v2_0_indy", + event_type=V20CredExRecordIndy, + ) + + holder_cred_ex = await holder.event_with_values( + topic="issue_credential_v2_0", + event_type=V20CredExRecord, + cred_ex_id=holder_cred_ex_id, + state="done", + ) + holder_indy_record = await holder.event_with_values( + topic="issue_credential_v2_0_anoncreds" + if is_holder_anoncreds + else "issue_credential_v2_0_indy", + event_type=V20CredExRecordIndy, + ) + + return ( + V20CredExRecordDetail(cred_ex_record=issuer_cred_ex, details=issuer_indy_record), + V20CredExRecordDetail( + cred_ex_record=holder_cred_ex, + details=holder_indy_record, + ), + ) + + +async def anoncreds_present_proof_v2( + holder: Controller, + verifier: Controller, + holder_connection_id: str, + verifier_connection_id: str, + *, + name: Optional[str] = None, + version: Optional[str] = None, + comment: Optional[str] = None, + requested_attributes: Optional[List[Mapping[str, Any]]] = None, + requested_predicates: Optional[List[Mapping[str, Any]]] = None, + non_revoked: Optional[Mapping[str, int]] = None, +): + """Present an credential using present proof v2.""" + + is_verifier_anoncreds = (await verifier.get("/settings", response=Settings)).get( + "wallet.type" + ) == "askar-anoncreds" + + attrs = { + "name": name or "proof", + "version": version or "0.1.0", + "nonce": str(randbelow(10**10)), + "requested_attributes": { + str(uuid4()): attr for attr in requested_attributes or [] + }, + "requested_predicates": { + str(uuid4()): pred for pred in requested_predicates or [] + }, + "non_revoked": (non_revoked if non_revoked else None), + } + + if is_verifier_anoncreds: + presentation_request = { + "anoncreds": attrs, + } + else: + presentation_request = { + "indy": attrs, + } + verifier_pres_ex = await verifier.post( + "/present-proof-2.0/send-request", + json={ + "auto_verify": False, + "comment": comment or "Presentation request from minimal", + "connection_id": verifier_connection_id, + "presentation_request": presentation_request, + "trace": False, + }, + response=V20PresExRecord, + ) + verifier_pres_ex_id = verifier_pres_ex.pres_ex_id + + holder_pres_ex = await holder.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + connection_id=holder_connection_id, + state="request-received", + ) + assert holder_pres_ex.pres_request + holder_pres_ex_id = holder_pres_ex.pres_ex_id + + relevant_creds = await holder.get( + f"/present-proof-2.0/records/{holder_pres_ex_id}/credentials", + response=List[CredPrecis], + ) + assert holder_pres_ex.by_format.pres_request + proof_request = holder_pres_ex.by_format.pres_request.get( + "anoncreds" + ) or holder_pres_ex.by_format.pres_request.get("indy") + pres_spec = auto_select_credentials_for_presentation_request( + proof_request, relevant_creds + ) + if is_verifier_anoncreds: + proof = {"anoncreds": pres_spec.serialize()} + else: + proof = {"indy": pres_spec.serialize()} + await holder.post( + f"/present-proof-2.0/records/{holder_pres_ex_id}/send-presentation", + json=proof, + response=V20PresExRecord, + ) + + await verifier.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + pres_ex_id=verifier_pres_ex_id, + state="presentation-received", + ) + await verifier.post( + f"/present-proof-2.0/records/{verifier_pres_ex_id}/verify-presentation", + json={}, + response=V20PresExRecord, + ) + verifier_pres_ex = await verifier.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + pres_ex_id=verifier_pres_ex_id, + state="done", + ) + + holder_pres_ex = await holder.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + pres_ex_id=holder_pres_ex_id, + state="done", + ) + + return holder_pres_ex, verifier_pres_ex