diff --git a/acapy_agent/anoncreds/base.py b/acapy_agent/anoncreds/base.py index 6cb5940bb2..32c0a1c1f1 100644 --- a/acapy_agent/anoncreds/base.py +++ b/acapy_agent/anoncreds/base.py @@ -6,8 +6,8 @@ from ..config.injection_context import InjectionContext from ..core.error import BaseError from ..core.profile import Profile -from .models.anoncreds_cred_def import CredDef, CredDefResult, GetCredDefResult -from .models.anoncreds_revocation import ( +from .models.credential_definition import CredDef, CredDefResult, GetCredDefResult +from .models.revocation import ( GetRevListResult, GetRevRegDefResult, RevList, @@ -15,7 +15,7 @@ RevRegDef, RevRegDefResult, ) -from .models.anoncreds_schema import AnonCredsSchema, GetSchemaResult, SchemaResult +from .models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult T = TypeVar("T") diff --git a/acapy_agent/anoncreds/default/did_cheqd/registry.py b/acapy_agent/anoncreds/default/did_cheqd/registry.py index c33d17ebe8..8ea5643deb 100644 --- a/acapy_agent/anoncreds/default/did_cheqd/registry.py +++ b/acapy_agent/anoncreds/default/did_cheqd/registry.py @@ -13,14 +13,14 @@ BaseAnonCredsResolver, AnonCredsRegistrationError, ) -from ...models.anoncreds_cred_def import ( +from ...models.credential_definition import ( CredDef, CredDefResult, GetCredDefResult, CredDefState, CredDefValue, ) -from ...models.anoncreds_revocation import ( +from ...models.revocation import ( GetRevListResult, GetRevRegDefResult, RevList, @@ -28,19 +28,18 @@ RevRegDef, RevRegDefResult, ) -from ...models.anoncreds_schema import ( +from ...models.schema import ( AnonCredsSchema, GetSchemaResult, SchemaResult, SchemaState, ) -from ....did.cheqd.cheqd_manager import DidCheqdManager +from ....did.cheqd.manager import DidCheqdManager from ....did.cheqd.registrar import DidCheqdRegistrar from ....resolver.default.cheqd import CheqdDIDResolver from ....messaging.valid import CheqdDID from ....wallet.base import BaseWallet from ....wallet.jwt import dict_to_b64 -from ....wallet.util import b64_to_bytes, bytes_to_b64 LOGGER = logging.getLogger(__name__) @@ -310,15 +309,12 @@ async def _create_and_publish_resource( LOGGER.debug("JOBID %s", job_id) if resource_state.get("state") == "action": - sign_req: dict = resource_state.get("signingRequest")[0] - kid: str = sign_req.get("kid") - payload_to_sign: str = sign_req.get("serializedPayload") - key = await wallet.get_key_by_kid(kid) - verkey = key.verkey - - # sign payload - signature_bytes = await wallet.sign_message( - b64_to_bytes(payload_to_sign), verkey + signing_requests: dict = resource_state.get("signingRequest") + if not signing_requests: + raise Exception("No signing requests available for update.") + # sign all requests + signed_responses = await DidCheqdManager.sign_requests( + wallet, signing_requests ) # publish resource @@ -326,14 +322,7 @@ async def _create_and_publish_resource( did, { "jobId": job_id, - "secret": { - "signingResponse": [ - { - "kid": kid, - "signature": bytes_to_b64(signature_bytes), - } - ], - }, + "secret": {"signingResponse": signed_responses}, }, ) resource_state = publish_resource_res.get("resourceState") diff --git a/acapy_agent/anoncreds/default/did_cheqd/tests/__init__.py b/acapy_agent/anoncreds/default/did_cheqd/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/acapy_agent/anoncreds/default/did_cheqd/tests/test_registry.py b/acapy_agent/anoncreds/default/did_cheqd/tests/test_registry.py new file mode 100644 index 0000000000..17695e6367 --- /dev/null +++ b/acapy_agent/anoncreds/default/did_cheqd/tests/test_registry.py @@ -0,0 +1,33 @@ +from unittest.async_case import IsolatedAsyncioTestCase + +import pytest +from acapy_agent.utils.testing import create_test_profile +from .....messaging.valid import CHEQD_DID_VALIDATE +from .....anoncreds.default.did_cheqd import registry as test_module + +TEST_CHEQD_DID = "did:cheqd:testnet:1686a962-6e82-46f3-bde7-e6711d63958c" +TEST_CHEQD_SCHEMA_ID = "did:cheqd:testnet:1686a962-6e82-46f3-bde7-e6711d63958c/resources/e788d345-dd0c-427a-a74b-27faf1e608cd" +TEST_CHEQD_CRED_DEF_ID = "did:cheqd:testnet:1686a962-6e82-46f3-bde7-e6711d63958c/resources/02229804-b46a-4be9-a6f1-13869109c7ea" +TEST_CHEQD_REV_REG_ENTRY = "did:cheqd:testnet:1686a962-6e82-46f3-bde7-e6711d63958c?resourceName=test&resourceType=anoncredsRevRegEntry" + + +@pytest.mark.anoncreds +class TestCheqdRegistry(IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.profile = await create_test_profile( + settings={"wallet.type": "askar-anoncreds"}, + ) + self.registry = test_module.DIDCheqdRegistry() + + async def test_supported_did_regex(self): + """Test the supported_did_regex.""" + + assert self.registry.supported_identifiers_regex == CHEQD_DID_VALIDATE.PATTERN + assert bool(self.registry.supported_identifiers_regex.match(TEST_CHEQD_DID)) + assert bool(self.registry.supported_identifiers_regex.match(TEST_CHEQD_SCHEMA_ID)) + assert bool( + self.registry.supported_identifiers_regex.match(TEST_CHEQD_CRED_DEF_ID) + ) + assert bool( + self.registry.supported_identifiers_regex.match(TEST_CHEQD_REV_REG_ENTRY) + ) diff --git a/acapy_agent/anoncreds/default/did_indy/registry.py b/acapy_agent/anoncreds/default/did_indy/registry.py index 567d90ccce..dcaafe4c06 100644 --- a/acapy_agent/anoncreds/default/did_indy/registry.py +++ b/acapy_agent/anoncreds/default/did_indy/registry.py @@ -7,8 +7,8 @@ from ....config.injection_context import InjectionContext from ....core.profile import Profile from ...base import BaseAnonCredsRegistrar, BaseAnonCredsResolver -from ...models.anoncreds_cred_def import CredDef, CredDefResult, GetCredDefResult -from ...models.anoncreds_revocation import ( +from ...models.credential_definition import CredDef, CredDefResult, GetCredDefResult +from ...models.revocation import ( GetRevListResult, GetRevRegDefResult, RevList, @@ -16,7 +16,7 @@ RevRegDef, RevRegDefResult, ) -from ...models.anoncreds_schema import AnonCredsSchema, GetSchemaResult, SchemaResult +from ...models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult LOGGER = logging.getLogger(__name__) diff --git a/acapy_agent/anoncreds/default/did_web/registry.py b/acapy_agent/anoncreds/default/did_web/registry.py index 63382f3606..f97ba88fb8 100644 --- a/acapy_agent/anoncreds/default/did_web/registry.py +++ b/acapy_agent/anoncreds/default/did_web/registry.py @@ -6,8 +6,8 @@ from ....config.injection_context import InjectionContext from ....core.profile import Profile from ...base import BaseAnonCredsRegistrar, BaseAnonCredsResolver -from ...models.anoncreds_cred_def import CredDef, CredDefResult, GetCredDefResult -from ...models.anoncreds_revocation import ( +from ...models.credential_definition import CredDef, CredDefResult, GetCredDefResult +from ...models.revocation import ( GetRevListResult, GetRevRegDefResult, RevList, @@ -15,7 +15,7 @@ RevRegDef, RevRegDefResult, ) -from ...models.anoncreds_schema import AnonCredsSchema, GetSchemaResult, SchemaResult +from ...models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult class DIDWebRegistry(BaseAnonCredsResolver, BaseAnonCredsRegistrar): diff --git a/acapy_agent/anoncreds/default/legacy_indy/recover.py b/acapy_agent/anoncreds/default/legacy_indy/recover.py index cd183adf15..4c3eaf0517 100644 --- a/acapy_agent/anoncreds/default/legacy_indy/recover.py +++ b/acapy_agent/anoncreds/default/legacy_indy/recover.py @@ -9,7 +9,7 @@ import indy_vdr from anoncreds import RevocationRegistry, RevocationRegistryDefinition -from ...models.anoncreds_revocation import RevList +from ...models.revocation import RevList LOGGER = logging.getLogger(__name__) diff --git a/acapy_agent/anoncreds/default/legacy_indy/registry.py b/acapy_agent/anoncreds/default/legacy_indy/registry.py index e173bbde32..aff1040616 100644 --- a/acapy_agent/anoncreds/default/legacy_indy/registry.py +++ b/acapy_agent/anoncreds/default/legacy_indy/registry.py @@ -56,14 +56,14 @@ ) from ...events import RevListFinishedEvent from ...issuer import CATEGORY_CRED_DEF, AnonCredsIssuer, AnonCredsIssuerError -from ...models.anoncreds_cred_def import ( +from ...models.credential_definition import ( CredDef, CredDefResult, CredDefState, CredDefValue, GetCredDefResult, ) -from ...models.anoncreds_revocation import ( +from ...models.revocation import ( GetRevListResult, GetRevRegDefResult, RevList, @@ -74,7 +74,7 @@ RevRegDefState, RevRegDefValue, ) -from ...models.anoncreds_schema import ( +from ...models.schema import ( AnonCredsSchema, GetSchemaResult, SchemaResult, diff --git a/acapy_agent/anoncreds/default/legacy_indy/tests/test_recover.py b/acapy_agent/anoncreds/default/legacy_indy/tests/test_recover.py index 61c0a90f40..b504690c5e 100644 --- a/acapy_agent/anoncreds/default/legacy_indy/tests/test_recover.py +++ b/acapy_agent/anoncreds/default/legacy_indy/tests/test_recover.py @@ -9,9 +9,8 @@ import pytest from anoncreds import RevocationRegistryDefinition -from acapy_agent.tests import mock - -from ....models.anoncreds_revocation import RevList, RevRegDef, RevRegDefValue +from .....tests import mock +from ....models.revocation import RevList, RevRegDef, RevRegDefValue from ..recover import ( RevocRecoveryException, _check_tails_hash_for_inconsistency, diff --git a/acapy_agent/anoncreds/default/legacy_indy/tests/test_registry.py b/acapy_agent/anoncreds/default/legacy_indy/tests/test_registry.py index 2b6b6c0b76..c41eb80797 100644 --- a/acapy_agent/anoncreds/default/legacy_indy/tests/test_registry.py +++ b/acapy_agent/anoncreds/default/legacy_indy/tests/test_registry.py @@ -16,25 +16,6 @@ from .....anoncreds.base import AnonCredsSchemaAlreadyExists from .....anoncreds.default.legacy_indy import registry as test_module from .....anoncreds.issuer import AnonCredsIssuer -from .....anoncreds.models.anoncreds_cred_def import ( - CredDef, - CredDefResult, - CredDefValue, - CredDefValuePrimary, -) -from .....anoncreds.models.anoncreds_revocation import ( - RevList, - RevListResult, - RevRegDef, - RevRegDefResult, - RevRegDefState, - RevRegDefValue, -) -from .....anoncreds.models.anoncreds_schema import ( - AnonCredsSchema, - GetSchemaResult, - SchemaResult, -) from .....askar.profile_anon import ( AskarAnoncredsProfileSession, ) @@ -55,6 +36,21 @@ ) from .....tests import mock from .....utils.testing import create_test_profile +from ....models.credential_definition import ( + CredDef, + CredDefResult, + CredDefValue, + CredDefValuePrimary, +) +from ....models.revocation import ( + RevList, + RevListResult, + RevRegDef, + RevRegDefResult, + RevRegDefState, + RevRegDefValue, +) +from ....models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult B58 = alphabet if isinstance(alphabet, str) else alphabet.decode("ascii") INDY_DID = rf"^(did:sov:)?[{B58}]{{21,22}}$" diff --git a/acapy_agent/anoncreds/events.py b/acapy_agent/anoncreds/events.py index 4511b546f7..98477bf4e1 100644 --- a/acapy_agent/anoncreds/events.py +++ b/acapy_agent/anoncreds/events.py @@ -4,7 +4,7 @@ from typing import NamedTuple, Optional from ..core.event_bus import Event -from .models.anoncreds_revocation import RevRegDef +from .models.revocation import RevRegDef CRED_DEF_FINISHED_EVENT = "anoncreds::credential-definition::finished" REV_REG_DEF_FINISHED_EVENT = "anoncreds::revocation-registry-definition::finished" diff --git a/acapy_agent/anoncreds/holder.py b/acapy_agent/anoncreds/holder.py index 04a1a16fac..9db575c806 100644 --- a/acapy_agent/anoncreds/holder.py +++ b/acapy_agent/anoncreds/holder.py @@ -3,7 +3,6 @@ import asyncio import json import logging -import re from typing import Dict, Optional, Sequence, Tuple, Union from anoncreds import ( @@ -23,7 +22,6 @@ from pyld.jsonld import JsonLdProcessor from uuid_utils import uuid4 -from ..anoncreds.models.anoncreds_schema import AnonCredsSchema from ..askar.profile_anon import AskarAnoncredsProfile from ..core.error import BaseError from ..core.profile import Profile @@ -33,7 +31,8 @@ from ..vc.vc_ld import VerifiableCredential from ..wallet.error import WalletNotFoundError from .error_messages import ANONCREDS_PROFILE_REQUIRED_MSG -from .models.anoncreds_cred_def import CredDef +from .models.credential_definition import CredDef +from .models.schema import AnonCredsSchema from .registry import AnonCredsRegistry LOGGER = logging.getLogger(__name__) @@ -150,8 +149,8 @@ async def create_credential_request( ) = await asyncio.get_event_loop().run_in_executor( None, CredentialRequest.create, - None, holder_did, + None, credential_definition.to_native(), secret, AnonCredsHolder.MASTER_SECRET_ID, @@ -231,25 +230,11 @@ async def _finish_store_credential( rev_reg_def: Optional[dict] = None, ) -> str: credential_data = cred_recvd.to_dict() - schema_id = cred_recvd.schema_id - schema_id_parts = re.match(r"^(\w+):2:([^:]+):([^:]+)$", schema_id) - if not schema_id_parts: - raise AnonCredsHolderError(f"Error parsing credential schema ID: {schema_id}") - cred_def_id = cred_recvd.cred_def_id - cdef_id_parts = re.match(r"^(\w+):3:CL:([^:]+):([^:]+)$", cred_def_id) - if not cdef_id_parts: - raise AnonCredsHolderError( - f"Error parsing credential definition ID: {cred_def_id}" - ) - credential_id = credential_id or str(uuid4()) tags = { - "schema_id": schema_id, - "schema_issuer_did": schema_id_parts[1], - "schema_name": schema_id_parts[2], - "schema_version": schema_id_parts[3], - "issuer_did": cdef_id_parts[1], - "cred_def_id": cred_def_id, + "schema_id": cred_recvd.schema_id, + "issuer_did": credential_definition["issuerId"], + "cred_def_id": cred_recvd.cred_def_id, "rev_reg_id": cred_recvd.rev_reg_id or "None", } diff --git a/acapy_agent/anoncreds/issuer.py b/acapy_agent/anoncreds/issuer.py index ad79173e16..6dc3fababa 100644 --- a/acapy_agent/anoncreds/issuer.py +++ b/acapy_agent/anoncreds/issuer.py @@ -24,8 +24,8 @@ from .base import AnonCredsSchemaAlreadyExists, BaseAnonCredsError from .error_messages import ANONCREDS_PROFILE_REQUIRED_MSG from .events import CredDefFinishedEvent -from .models.anoncreds_cred_def import CredDef, CredDefResult -from .models.anoncreds_schema import AnonCredsSchema, SchemaResult, SchemaState +from .models.credential_definition import CredDef, CredDefResult +from .models.schema import AnonCredsSchema, SchemaResult, SchemaState from .registry import AnonCredsRegistry LOGGER = logging.getLogger(__name__) diff --git a/acapy_agent/anoncreds/models/credential.py b/acapy_agent/anoncreds/models/credential.py new file mode 100644 index 0000000000..c008fc16c7 --- /dev/null +++ b/acapy_agent/anoncreds/models/credential.py @@ -0,0 +1,167 @@ +"""Credential artifacts.""" + +from typing import Mapping, Optional + +from marshmallow import EXCLUDE, ValidationError, fields + +from ...messaging.models.base import BaseModel, BaseModelSchema +from ...messaging.valid import ( + ANONCREDS_CRED_DEF_ID_EXAMPLE, + ANONCREDS_CRED_DEF_ID_VALIDATE, + ANONCREDS_REV_REG_ID_EXAMPLE, + ANONCREDS_REV_REG_ID_VALIDATE, + ANONCREDS_SCHEMA_ID_EXAMPLE, + ANONCREDS_SCHEMA_ID_VALIDATE, + NUM_STR_ANY_EXAMPLE, + NUM_STR_ANY_VALIDATE, +) + + +class AnoncredsAttrValue(BaseModel): + """Anoncreds attribute value.""" + + class Meta: + """Anoncreds attribute value.""" + + schema_class = "AnoncredsAttrValueSchema" + + def __init__( + self, raw: Optional[str] = None, encoded: Optional[str] = None, **kwargs + ): + """Initialize anoncreds (credential) attribute value.""" + super().__init__(**kwargs) + self.raw = raw + self.encoded = encoded + + +class AnoncredsAttrValueSchema(BaseModelSchema): + """Anoncreds attribute value schema.""" + + class Meta: + """Anoncreds attribute value schema metadata.""" + + model_class = AnoncredsAttrValue + unknown = EXCLUDE + + raw = fields.Str(required=True, metadata={"description": "Attribute raw value"}) + encoded = fields.Str( + required=True, + validate=NUM_STR_ANY_VALIDATE, + metadata={ + "description": "Attribute encoded value", + "example": NUM_STR_ANY_EXAMPLE, + }, + ) + + +class DictWithAnoncredsAttrValueSchema(fields.Dict): + """Dict with anoncreds attribute value schema.""" + + def __init__(self, **kwargs): + """Initialize the custom schema for a dictionary with AnoncredsAttrValue.""" + super().__init__( + keys=fields.Str(metadata={"description": "Attribute name"}), + values=fields.Nested(AnoncredsAttrValueSchema()), + **kwargs, + ) + + def _deserialize(self, value, attr, data, **kwargs): + """Deserialize dict with anoncreds attribute value.""" + if not isinstance(value, dict): + raise ValidationError("Value must be a dict.") + + errors = {} + anoncreds_attr_value_schema = AnoncredsAttrValueSchema() + + for k, v in value.items(): + if isinstance(v, dict): + validation_errors = anoncreds_attr_value_schema.validate(v) + if validation_errors: + errors[k] = validation_errors + + if errors: + raise ValidationError(errors) + + return value + + +class AnoncredsCredential(BaseModel): + """Anoncreds credential.""" + + class Meta: + """Anoncreds credential metadata.""" + + schema_class = "AnoncredsCredentialSchema" + + def __init__( + self, + schema_id: Optional[str] = None, + cred_def_id: Optional[str] = None, + rev_reg_id: Optional[str] = None, + values: Mapping[str, AnoncredsAttrValue] = None, + signature: Optional[Mapping] = None, + signature_correctness_proof: Optional[Mapping] = None, + rev_reg: Optional[Mapping] = None, + witness: Optional[Mapping] = None, + ): + """Initialize anoncreds credential.""" + self.schema_id = schema_id + self.cred_def_id = cred_def_id + self.rev_reg_id = rev_reg_id + self.values = values + self.signature = signature + self.signature_correctness_proof = signature_correctness_proof + self.rev_reg = rev_reg + self.witness = witness + + +class AnoncredsCredentialSchema(BaseModelSchema): + """Anoncreds credential schema.""" + + class Meta: + """Anoncreds credential schemametadata.""" + + model_class = AnoncredsCredential + unknown = EXCLUDE + + schema_id = fields.Str( + required=True, + validate=ANONCREDS_SCHEMA_ID_VALIDATE, + metadata={ + "description": "Schema identifier", + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, + }, + ) + cred_def_id = fields.Str( + required=True, + validate=ANONCREDS_CRED_DEF_ID_VALIDATE, + metadata={ + "description": "Credential definition identifier", + "example": ANONCREDS_CRED_DEF_ID_EXAMPLE, + }, + ) + rev_reg_id = fields.Str( + allow_none=True, + validate=ANONCREDS_REV_REG_ID_VALIDATE, + metadata={ + "description": "Revocation registry identifier", + "example": ANONCREDS_REV_REG_ID_EXAMPLE, + }, + ) + values = DictWithAnoncredsAttrValueSchema( + required=True, + metadata={"description": "Credential attributes"}, + ) + signature = fields.Dict( + required=True, metadata={"description": "Credential signature"} + ) + signature_correctness_proof = fields.Dict( + required=True, + metadata={"description": "Credential signature correctness proof"}, + ) + rev_reg = fields.Dict( + allow_none=True, metadata={"description": "Revocation registry state"} + ) + witness = fields.Dict( + allow_none=True, metadata={"description": "Witness for revocation proof"} + ) diff --git a/acapy_agent/anoncreds/models/anoncreds_cred_def.py b/acapy_agent/anoncreds/models/credential_definition.py similarity index 97% rename from acapy_agent/anoncreds/models/anoncreds_cred_def.py rename to acapy_agent/anoncreds/models/credential_definition.py index 17d41098e7..4e543eaf57 100644 --- a/acapy_agent/anoncreds/models/anoncreds_cred_def.py +++ b/acapy_agent/anoncreds/models/credential_definition.py @@ -9,9 +9,9 @@ from ...messaging.models.base import BaseModel, BaseModelSchema from ...messaging.valid import ( - INDY_CRED_DEF_ID_EXAMPLE, - INDY_OR_KEY_DID_EXAMPLE, - INDY_SCHEMA_ID_EXAMPLE, + ANONCREDS_CRED_DEF_ID_EXAMPLE, + ANONCREDS_DID_EXAMPLE, + ANONCREDS_SCHEMA_ID_EXAMPLE, NUM_STR_WHOLE_EXAMPLE, NUM_STR_WHOLE_VALIDATE, ) @@ -256,7 +256,7 @@ class Meta: issuer_id = fields.Str( metadata={ "description": "Issuer Identifier of the credential definition or schema", - "example": INDY_OR_KEY_DID_EXAMPLE, + "example": ANONCREDS_DID_EXAMPLE, }, data_key="issuerId", ) @@ -264,7 +264,7 @@ class Meta: data_key="schemaId", metadata={ "description": "Schema identifier", - "example": INDY_SCHEMA_ID_EXAMPLE, + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, }, ) type = fields.Str(validate=OneOf(["CL"])) @@ -333,7 +333,7 @@ class Meta: credential_definition_id = fields.Str( metadata={ "description": "credential definition id", - "example": INDY_CRED_DEF_ID_EXAMPLE, + "example": ANONCREDS_CRED_DEF_ID_EXAMPLE, }, allow_none=True, ) @@ -434,7 +434,7 @@ class Meta: credential_definition_id = fields.Str( metadata={ "description": "credential definition id", - "example": INDY_CRED_DEF_ID_EXAMPLE, + "example": ANONCREDS_CRED_DEF_ID_EXAMPLE, }, ) credential_definition = fields.Nested( diff --git a/acapy_agent/anoncreds/models/credential_offer.py b/acapy_agent/anoncreds/models/credential_offer.py new file mode 100644 index 0000000000..c77419eba3 --- /dev/null +++ b/acapy_agent/anoncreds/models/credential_offer.py @@ -0,0 +1,149 @@ +"""Anoncreds Credential Offer format for v2.0 of the issue-credential protocol.""" + +from typing import Optional, Sequence + +from marshmallow import EXCLUDE, fields + +from ...messaging.models.base import BaseModel, BaseModelSchema +from ...messaging.valid import ( + ANONCREDS_CRED_DEF_ID_EXAMPLE, + ANONCREDS_CRED_DEF_ID_VALIDATE, + ANONCREDS_SCHEMA_ID_EXAMPLE, + ANONCREDS_SCHEMA_ID_VALIDATE, + NUM_STR_WHOLE_EXAMPLE, + NUM_STR_WHOLE_VALIDATE, +) + + +class AnoncredsKeyCorrectnessProof(BaseModel): + """Anoncreds key correctness proof.""" + + class Meta: + """AnoncredsKeyCorrectnessProof metadata.""" + + schema_class = "AnoncredsKeyCorrectnessProofSchema" + + def __init__( + self, + c: Optional[str] = None, + xz_cap: Optional[str] = None, + xr_cap: Sequence[Sequence[str]] = None, + **kwargs, + ): + """Initialize XR cap for anoncreds key correctness proof.""" + super().__init__(**kwargs) + + self.c = c + self.xz_cap = xz_cap + self.xr_cap = xr_cap + + +class AnoncredsCorrectnessProofSchema(BaseModelSchema): + """Anoncreds key correctness proof schema.""" + + class Meta: + """Anoncreds key correctness proof schema metadata.""" + + model_class = AnoncredsKeyCorrectnessProof + unknown = EXCLUDE + + c = fields.Str( + required=True, + validate=NUM_STR_WHOLE_VALIDATE, + metadata={ + "description": "c in key correctness proof", + "example": NUM_STR_WHOLE_EXAMPLE, + }, + ) + xz_cap = fields.Str( + required=True, + validate=NUM_STR_WHOLE_VALIDATE, + metadata={ + "description": "xz_cap in key correctness proof", + "example": NUM_STR_WHOLE_EXAMPLE, + }, + ) + xr_cap = fields.List( + fields.List( + fields.Str( + required=True, + metadata={ + "description": "xr_cap component values in key correctness proof" + }, + ), + required=True, + metadata={ + "description": "xr_cap components in key correctness proof", + "many": True, + }, + ), + required=True, + metadata={"description": "xr_cap in key correctness proof", "many": True}, + ) + + +class AnoncredsCredentialOffer(BaseModel): + """Anoncreds Credential Offer.""" + + class Meta: + """AnoncredsCredentialOffer metadata.""" + + schema_class = "AnoncredsCredentialOfferSchema" + + def __init__( + self, + schema_id: Optional[str] = None, + cred_def_id: Optional[str] = None, + nonce: Optional[str] = None, + key_correctness_proof: Optional[str] = None, + **kwargs, + ): + """Initialize values .""" + super().__init__(**kwargs) + self.schema_id = schema_id + self.cred_def_id = cred_def_id + self.nonce = nonce + self.key_correctness_proof = key_correctness_proof + + +class AnoncredsCredentialOfferSchema(BaseModelSchema): + """Anoncreds Credential Offer Schema.""" + + class Meta: + """AnoncredsCredentialOffer schema metadata.""" + + model_class = AnoncredsCredentialOffer + unknown = EXCLUDE + + schema_id = fields.Str( + required=True, + validate=ANONCREDS_SCHEMA_ID_VALIDATE, + metadata={ + "description": "Schema identifier", + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, + }, + ) + + cred_def_id = fields.Str( + required=True, + validate=ANONCREDS_CRED_DEF_ID_VALIDATE, + metadata={ + "description": "Credential definition identifier", + "example": ANONCREDS_CRED_DEF_ID_EXAMPLE, + }, + ) + + nonce = fields.Str( + required=True, + validate=NUM_STR_WHOLE_VALIDATE, + metadata={ + "description": "Nonce in credential abstract", + "example": NUM_STR_WHOLE_EXAMPLE, + }, + ) + + key_correctness_proof = fields.Nested( + AnoncredsCorrectnessProofSchema(), + required=True, + metadata={"description": "Key correctness proof"}, + ) diff --git a/acapy_agent/anoncreds/models/credential_proposal.py b/acapy_agent/anoncreds/models/credential_proposal.py new file mode 100644 index 0000000000..3d41d08747 --- /dev/null +++ b/acapy_agent/anoncreds/models/credential_proposal.py @@ -0,0 +1,58 @@ +"""Anoncreds credential definition proposal.""" + +import re + +from marshmallow import fields + +from ...core.profile import Profile +from ...messaging.models.openapi import OpenAPISchema +from ...messaging.valid import ( + ANONCREDS_CRED_DEF_ID_EXAMPLE, + ANONCREDS_CRED_DEF_ID_VALIDATE, + ANONCREDS_DID_EXAMPLE, + ANONCREDS_DID_VALIDATE, + ANONCREDS_SCHEMA_ID_EXAMPLE, + ANONCREDS_SCHEMA_ID_VALIDATE, +) + + +class AnoncredsCredentialDefinitionProposal(OpenAPISchema): + """Query string parameters for credential definition searches.""" + + schema_id = fields.Str( + required=False, + validate=ANONCREDS_SCHEMA_ID_VALIDATE, + metadata={ + "description": "Schema identifier", + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, + }, + ) + issuer_id = fields.Str( + required=False, + validate=ANONCREDS_DID_VALIDATE, + metadata={"description": "Issuer DID", "example": ANONCREDS_DID_EXAMPLE}, + ) + cred_def_id = fields.Str( + required=False, + validate=ANONCREDS_CRED_DEF_ID_VALIDATE, + metadata={ + "description": "Credential definition id", + "example": ANONCREDS_CRED_DEF_ID_EXAMPLE, + }, + ) + + +CRED_DEF_TAGS = list( + vars(AnoncredsCredentialDefinitionProposal).get("_declared_fields", []) +) + +CRED_DEF_EVENT_PREFIX = "acapy::CRED_DEF::" +EVENT_LISTENER_PATTERN = re.compile(f"^{CRED_DEF_EVENT_PREFIX}(.*)?$") + + +async def notify_cred_def_event(profile: Profile, cred_def_id: str, meta_data: dict): + """Send notification for a cred def post-process event.""" + await profile.notify( + CRED_DEF_EVENT_PREFIX + cred_def_id, + meta_data, + ) diff --git a/acapy_agent/anoncreds/models/credential_request.py b/acapy_agent/anoncreds/models/credential_request.py new file mode 100644 index 0000000000..83b0d350c0 --- /dev/null +++ b/acapy_agent/anoncreds/models/credential_request.py @@ -0,0 +1,81 @@ +"""Cred request artifacts to attach to RFC 453 messages.""" + +from typing import Mapping, Optional + +from marshmallow import EXCLUDE, fields + +from ...messaging.models.base import BaseModel, BaseModelSchema +from ...messaging.valid import ( + ANONCREDS_CRED_DEF_ID_EXAMPLE, + ANONCREDS_CRED_DEF_ID_VALIDATE, + NUM_STR_WHOLE_EXAMPLE, + NUM_STR_WHOLE_VALIDATE, + UUID4_EXAMPLE, +) + + +class AnoncredsCredRequest(BaseModel): + """Anoncreds credential request.""" + + class Meta: + """Anoncreds credential request metadata.""" + + schema_class = "AnoncredsCredRequestSchema" + + def __init__( + self, + entropy: Optional[str] = None, + cred_def_id: Optional[str] = None, + blinded_ms: Optional[Mapping] = None, + blinded_ms_correctness_proof: Optional[Mapping] = None, + nonce: Optional[str] = None, + **kwargs, + ): + """Initialize anoncreds credential request.""" + super().__init__(**kwargs) + self.entropy = entropy + self.cred_def_id = cred_def_id + self.blinded_ms = blinded_ms + self.blinded_ms_correctness_proof = blinded_ms_correctness_proof + self.nonce = nonce + + +class AnoncredsCredRequestSchema(BaseModelSchema): + """Anoncreds credential request schema.""" + + class Meta: + """Anoncreds credential request schema metadata.""" + + model_class = AnoncredsCredRequest + unknown = EXCLUDE + + entropy = fields.Str( + required=True, + metadata={ + "description": "Prover DID/Random String/UUID", + "example": UUID4_EXAMPLE, + }, + ) + cred_def_id = fields.Str( + required=True, + validate=ANONCREDS_CRED_DEF_ID_VALIDATE, + metadata={ + "description": "Credential definition identifier", + "example": ANONCREDS_CRED_DEF_ID_EXAMPLE, + }, + ) + blinded_ms = fields.Dict( + required=True, metadata={"description": "Blinded master secret"} + ) + blinded_ms_correctness_proof = fields.Dict( + required=True, + metadata={"description": "Blinded master secret correctness proof"}, + ) + nonce = fields.Str( + required=True, + validate=NUM_STR_WHOLE_VALIDATE, + metadata={ + "description": "Nonce in credential request", + "example": NUM_STR_WHOLE_EXAMPLE, + }, + ) diff --git a/acapy_agent/anoncreds/models/non_rev_interval.py b/acapy_agent/anoncreds/models/non_rev_interval.py new file mode 100644 index 0000000000..a224891189 --- /dev/null +++ b/acapy_agent/anoncreds/models/non_rev_interval.py @@ -0,0 +1,78 @@ +"""Anoncreds non-revocation interval.""" + +from time import time +from typing import Optional + +from marshmallow import EXCLUDE, fields + +from ...messaging.models.base import BaseModel, BaseModelSchema +from ...messaging.valid import INT_EPOCH_EXAMPLE, INT_EPOCH_VALIDATE + + +class AnoncredsNonRevocationInterval(BaseModel): + """Anoncreds non-revocation interval.""" + + class Meta: + """NonRevocationInterval metadata.""" + + schema_class = "AnoncredsNonRevocationIntervalSchema" + + def __init__(self, fro: Optional[int] = None, to: Optional[int] = None, **kwargs): + """Initialize non-revocation interval. + + Args: + fro: earliest time of interest + to: latest time of interest + kwargs: additional attributes + + """ + super().__init__(**kwargs) + self.fro = fro + self.to = to + + def covers(self, timestamp: Optional[int] = None) -> bool: + """Whether input timestamp (default now) lies within non-revocation interval. + + Args: + timestamp: time of interest + + Returns: + whether input time satisfies non-revocation interval + + """ + timestamp = timestamp or int(time()) + return (self.fro or 0) <= timestamp <= (self.to or timestamp) + + def timestamp(self) -> bool: + """Return a timestamp that the non-revocation interval covers.""" + return self.to or self.fro or int(time()) + + +class AnoncredsNonRevocationIntervalSchema(BaseModelSchema): + """Schema to allow serialization/deserialization of non-revocation intervals.""" + + class Meta: + """AnoncredsNonRevocationIntervalSchema metadata.""" + + model_class = AnoncredsNonRevocationInterval + unknown = EXCLUDE + + fro = fields.Int( + required=False, + data_key="from", + validate=INT_EPOCH_VALIDATE, + metadata={ + "description": "Earliest time of interest in non-revocation interval", + "strict": True, + "example": INT_EPOCH_EXAMPLE, + }, + ) + to = fields.Int( + required=False, + validate=INT_EPOCH_VALIDATE, + metadata={ + "description": "Latest time of interest in non-revocation interval", + "strict": True, + "example": INT_EPOCH_EXAMPLE, + }, + ) diff --git a/acapy_agent/anoncreds/models/predicate.py b/acapy_agent/anoncreds/models/predicate.py new file mode 100644 index 0000000000..9810a7bb78 --- /dev/null +++ b/acapy_agent/anoncreds/models/predicate.py @@ -0,0 +1,82 @@ +"""Utilities for dealing with predicates.""" + +from collections import namedtuple +from enum import Enum +from typing import Any + +Relation = namedtuple("Relation", "fortran wql math yes no") + + +class Predicate(Enum): + """Enum for predicate types that anoncreds-sdk supports.""" + + LT = Relation( + "LT", + "$lt", + "<", + lambda x, y: Predicate.to_int(x) < Predicate.to_int(y), + lambda x, y: Predicate.to_int(x) >= Predicate.to_int(y), + ) + LE = Relation( + "LE", + "$lte", + "<=", + lambda x, y: Predicate.to_int(x) <= Predicate.to_int(y), + lambda x, y: Predicate.to_int(x) > Predicate.to_int(y), + ) + GE = Relation( + "GE", + "$gte", + ">=", + lambda x, y: Predicate.to_int(x) >= Predicate.to_int(y), + lambda x, y: Predicate.to_int(x) < Predicate.to_int(y), + ) + GT = Relation( + "GT", + "$gt", + ">", + lambda x, y: Predicate.to_int(x) > Predicate.to_int(y), + lambda x, y: Predicate.to_int(x) <= Predicate.to_int(y), + ) + + @property + def fortran(self) -> str: + """Fortran nomenclature.""" + return self.value.fortran + + @property + def wql(self) -> str: + """WQL nomenclature.""" + return self.value.wql + + @property + def math(self) -> str: + """Mathematical nomenclature.""" + return self.value.math + + @staticmethod + def get(relation: str) -> "Predicate": + """Return enum instance corresponding to input relation string.""" + + for pred in Predicate: + if relation.upper() in ( + pred.value.fortran, + pred.value.wql.upper(), + pred.value.math, + ): + return pred + return None + + @staticmethod + def to_int(value: Any) -> int: + """Cast a value as its equivalent int for anoncreds predicate argument. + + Raise ValueError for any input but int, stringified int, or boolean. + + Args: + value: value to coerce + """ + + if isinstance(value, (bool, int)): + return int(value) + return int(str(value)) # kick out floats diff --git a/acapy_agent/anoncreds/models/presentation_request.py b/acapy_agent/anoncreds/models/presentation_request.py new file mode 100644 index 0000000000..05881dfdc8 --- /dev/null +++ b/acapy_agent/anoncreds/models/presentation_request.py @@ -0,0 +1,306 @@ +"""Classes to represent anoncreds presentation request.""" + +from typing import Mapping, Optional + +from marshmallow import ( + EXCLUDE, + Schema, + ValidationError, + fields, + validate, + validates_schema, +) + +from ...messaging.models.base import BaseModel, BaseModelSchema +from ...messaging.models.openapi import OpenAPISchema +from ...messaging.valid import ( + ANONCREDS_CRED_DEF_ID_EXAMPLE, + INT_EPOCH_EXAMPLE, + INT_EPOCH_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, + NUM_STR_NATURAL_EXAMPLE, + NUM_STR_NATURAL_VALIDATE, + PREDICATE_EXAMPLE, + PREDICATE_VALIDATE, +) + + +class AnoncredsPresentationReqPredSpecSchema(OpenAPISchema): + """Schema for predicate specification in anoncreds proof request.""" + + name = fields.Str( + required=True, metadata={"example": "index", "description": "Attribute name"} + ) + p_type = fields.Str( + required=True, + validate=PREDICATE_VALIDATE, + metadata={ + "description": "Predicate type ('<', '<=', '>=', or '>')", + "example": PREDICATE_EXAMPLE, + }, + ) + p_value = fields.Int( + required=True, metadata={"description": "Threshold value", "strict": True} + ) + restrictions = fields.List( + fields.Dict( + keys=fields.Str( + validate=validate.Regexp( + "^schema_id|schema_issuer_did|schema_name|schema_version|issuer_did|" + "cred_def_id|attr::.+::value$" + ), + metadata={"example": "cred_def_id"}, + ), + values=fields.Str(metadata={"example": ANONCREDS_CRED_DEF_ID_EXAMPLE}), + ), + required=False, + metadata={ + "description": ( + "If present, credential must satisfy one of given restrictions: specify" + " schema_id, schema_issuer_did, schema_name, schema_version," + " issuer_did, cred_def_id, and/or attr::::value where" + " represents a credential attribute name" + ) + }, + ) + non_revoked = fields.Nested( + Schema.from_dict( + { + "from": fields.Int( + required=False, + validate=INT_EPOCH_VALIDATE, + metadata={ + "description": ( + "Earliest time of interest in non-revocation interval" + ), + "strict": True, + "example": INT_EPOCH_EXAMPLE, + }, + ), + "to": fields.Int( + required=False, + validate=INT_EPOCH_VALIDATE, + metadata={ + "description": ( + "Latest time of interest in non-revocation interval" + ), + "strict": True, + "example": INT_EPOCH_EXAMPLE, + }, + ), + }, + name="AnoncredsPresentationReqPredSpecNonRevokedSchema", + ), + allow_none=True, + required=False, + ) + + +class AnoncredsPresentationReqAttrSpecSchema(OpenAPISchema): + """Schema for attribute specification in anoncreds proof request.""" + + name = fields.Str( + required=False, + metadata={"example": "favouriteDrink", "description": "Attribute name"}, + ) + names = fields.List( + fields.Str(metadata={"example": "age"}), + required=False, + metadata={"description": "Attribute name group"}, + ) + restrictions = fields.List( + fields.Dict( + keys=fields.Str( + validate=validate.Regexp( + "^schema_id|schema_issuer_did|schema_name|schema_version|issuer_did|" + "cred_def_id|attr::.+::value$" + ), + metadata={"example": "cred_def_id"}, + ), + values=fields.Str(metadata={"example": ANONCREDS_CRED_DEF_ID_EXAMPLE}), + ), + required=False, + metadata={ + "description": ( + "If present, credential must satisfy one of given restrictions: specify" + " schema_id, schema_issuer_did, schema_name, schema_version," + " issuer_did, cred_def_id, and/or attr::::value where" + " represents a credential attribute name" + ) + }, + ) + non_revoked = fields.Nested( + Schema.from_dict( + { + "from": fields.Int( + required=False, + validate=INT_EPOCH_VALIDATE, + metadata={ + "description": ( + "Earliest time of interest in non-revocation interval" + ), + "strict": True, + "example": INT_EPOCH_EXAMPLE, + }, + ), + "to": fields.Int( + required=False, + validate=INT_EPOCH_VALIDATE, + metadata={ + "description": ( + "Latest time of interest in non-revocation interval" + ), + "strict": True, + "example": INT_EPOCH_EXAMPLE, + }, + ), + }, + name="AnoncredsPresentationReqAttrSpecNonRevokedSchema", + ), + allow_none=True, + required=False, + ) + + @validates_schema + def validate_fields(self, data, **kwargs): + """Validate schema fields. + + Data must have exactly one of name or names; if names then restrictions are + mandatory. + + Args: + data: The data to validate + kwargs: Additional keyword arguments + + Raises: + ValidationError: if data has both or neither of name and names + + """ + if ("name" in data) == ("names" in data): + raise ValidationError( + "Attribute specification must have either name or names but not both" + ) + restrictions = data.get("restrictions") + if ("names" in data) and (not restrictions or all(not r for r in restrictions)): + raise ValidationError( + "Attribute specification on 'names' must have non-empty restrictions" + ) + + +class AnoncredsPresentationRequest(BaseModel): + """anoncreds proof request.""" + + class Meta: + """Anoncreds proof request metadata.""" + + schema_class = "AnoncredsPresentationRequestSchema" + + def __init__( + self, + nonce: Optional[str] = None, + name: Optional[str] = None, + version: Optional[str] = None, + requested_attributes: Optional[Mapping] = None, + requested_predicates: Optional[Mapping] = None, + non_revoked: Optional[Mapping] = None, + **kwargs, + ): + """Initialize anoncreds cred abstract object. + + Args: + nonce (str): The nonce value. + name (str): The name of the proof request. + version (str): The version of the proof request. + requested_attributes (Mapping): A mapping of attribute names to attribute + constraints. + requested_predicates (Mapping): A mapping of predicate names to predicate + constraints. + non_revoked (Mapping): A mapping of non-revocation timestamps. + kwargs: Keyword arguments for BaseModel + + """ + super().__init__(**kwargs) + self.nonce = nonce + self.name = name + self.version = version + self.requested_attributes = requested_attributes + self.requested_predicates = requested_predicates + self.non_revoked = non_revoked + + +class AnoncredsPresentationRequestSchema(BaseModelSchema): + """Schema for anoncreds proof request.""" + + class Meta: + """Anoncreds proof request schema metadata.""" + + model_class = AnoncredsPresentationRequest + unknown = EXCLUDE + + nonce = fields.Str( + required=False, + validate=NUM_STR_NATURAL_VALIDATE, + metadata={"description": "Nonce", "example": NUM_STR_NATURAL_EXAMPLE}, + ) + name = fields.Str( + required=False, + dump_default="Proof request", + metadata={"description": "Proof request name", "example": "Proof request"}, + ) + version = fields.Str( + required=False, + dump_default="1.0", + validate=MAJOR_MINOR_VERSION_VALIDATE, + metadata={ + "description": "Proof request version", + "example": MAJOR_MINOR_VERSION_EXAMPLE, + }, + ) + requested_attributes = fields.Dict( + required=True, + keys=fields.Str( + metadata={"decription": "Attribute referent", "example": "0_legalname_uuid"} + ), + values=fields.Nested(AnoncredsPresentationReqAttrSpecSchema()), + metadata={"description": "Requested attribute specifications of proof request"}, + ) + requested_predicates = fields.Dict( + required=True, + keys=fields.Str( + metadata={"description": "Predicate referent", "example": "0_age_GE_uuid"} + ), + values=fields.Nested(AnoncredsPresentationReqPredSpecSchema()), + metadata={"description": "Requested predicate specifications of proof request"}, + ) + non_revoked = fields.Nested( + Schema.from_dict( + { + "from": fields.Int( + required=False, + validate=INT_EPOCH_VALIDATE, + metadata={ + "description": ( + "Earliest time of interest in non-revocation interval" + ), + "strict": True, + "example": INT_EPOCH_EXAMPLE, + }, + ), + "to": fields.Int( + required=False, + validate=INT_EPOCH_VALIDATE, + metadata={ + "description": ( + "Latest time of interest in non-revocation interval" + ), + "strict": True, + "example": INT_EPOCH_EXAMPLE, + }, + ), + }, + name="AnoncredPresentationRequestNonRevokedSchema", + ), + allow_none=True, + required=False, + ) diff --git a/acapy_agent/anoncreds/models/proof.py b/acapy_agent/anoncreds/models/proof.py new file mode 100644 index 0000000000..817b947cc5 --- /dev/null +++ b/acapy_agent/anoncreds/models/proof.py @@ -0,0 +1,749 @@ +"""Marshmallow bindings for anoncreds proofs.""" + +from typing import Mapping, Optional, Sequence + +from marshmallow import EXCLUDE, fields, validate + +from ...messaging.models.base import BaseModel, BaseModelSchema +from ...messaging.valid import ( + ANONCREDS_CRED_DEF_ID_EXAMPLE, + ANONCREDS_CRED_DEF_ID_VALIDATE, + ANONCREDS_REV_REG_ID_EXAMPLE, + ANONCREDS_REV_REG_ID_VALIDATE, + ANONCREDS_SCHEMA_ID_EXAMPLE, + ANONCREDS_SCHEMA_ID_VALIDATE, + INT_EPOCH_EXAMPLE, + INT_EPOCH_VALIDATE, + NUM_STR_ANY_EXAMPLE, + NUM_STR_ANY_VALIDATE, + NUM_STR_WHOLE_EXAMPLE, + NUM_STR_WHOLE_VALIDATE, +) +from ...utils.tracing import AdminAPIMessageTracingSchema +from .predicate import Predicate +from .requested_credentials import ( + AnoncredsRequestedCredsRequestedAttrSchema, + AnoncredsRequestedCredsRequestedPredSchema, +) + + +class AnoncredsEQProof(BaseModel): + """Equality proof for anoncreds primary proof.""" + + class Meta: + """Equality proof metadata.""" + + schema_class = "AnoncredsEQProofMeta" + + def __init__( + self, + revealed_attrs: Mapping[str, str] = None, + a_prime: Optional[str] = None, + e: Optional[str] = None, + v: Optional[str] = None, + m: Mapping[str, str] = None, + m2: Optional[str] = None, + **kwargs, + ): + """Initialize equality proof object.""" + super().__init__(**kwargs) + self.revealed_attrs = revealed_attrs + self.a_prime = a_prime + self.e = e + self.v = v + self.m = m + self.m2 = m2 + + +class AnoncredsEQProofSchema(BaseModelSchema): + """Anoncreds equality proof schema.""" + + class Meta: + """Anoncreds equality proof metadata.""" + + model_class = AnoncredsEQProof + unknown = EXCLUDE + + revealed_attrs = fields.Dict( + keys=fields.Str(metadata={"example": "preference"}), + values=fields.Str( + validate=NUM_STR_ANY_VALIDATE, metadata={"example": NUM_STR_ANY_EXAMPLE} + ), + ) + a_prime = fields.Str( + validate=NUM_STR_WHOLE_VALIDATE, metadata={"example": NUM_STR_WHOLE_EXAMPLE} + ) + e = fields.Str( + validate=NUM_STR_WHOLE_VALIDATE, metadata={"example": NUM_STR_WHOLE_EXAMPLE} + ) + v = fields.Str( + validate=NUM_STR_WHOLE_VALIDATE, metadata={"example": NUM_STR_WHOLE_EXAMPLE} + ) + m = fields.Dict( + keys=fields.Str(metadata={"example": "master_secret"}), + values=fields.Str( + validate=NUM_STR_WHOLE_VALIDATE, metadata={"example": NUM_STR_WHOLE_EXAMPLE} + ), + ) + m2 = fields.Str( + validate=NUM_STR_WHOLE_VALIDATE, metadata={"example": NUM_STR_WHOLE_EXAMPLE} + ) + + +class AnoncredsGEProofPred(BaseModel): + """Anoncreds GE proof predicate.""" + + class Meta: + """Anoncreds GE proof predicate metadata.""" + + schema_class = "AnoncredsGEProofPredSchema" + + def __init__( + self, + attr_name: Optional[str] = None, + p_type: Optional[str] = None, + value: Optional[int] = None, + **kwargs, + ): + """Initialize anoncreds GE proof predicate.""" + super().__init__(**kwargs) + self.attr_name = attr_name + self.p_type = p_type + self.value = value + + +class AnoncredsGEProofPredSchema(BaseModelSchema): + """Anoncreds GE proof predicate schema.""" + + class Meta: + """Anoncreds GE proof predicate metadata.""" + + model_class = AnoncredsGEProofPred + unknown = EXCLUDE + + attr_name = fields.Str( + metadata={"description": "Attribute name, anoncreds-canonicalized"} + ) + p_type = fields.Str( + validate=validate.OneOf([p.fortran for p in Predicate]), + metadata={"description": "Predicate type"}, + ) + value = fields.Integer( + metadata={"strict": True, "description": "Predicate threshold value"} + ) + + +class AnoncredsGEProof(BaseModel): + """Greater-than-or-equal-to proof for anoncreds primary proof.""" + + class Meta: + """GE proof metadata.""" + + schema_class = "AnoncredsGEProofMeta" + + def __init__( + self, + u: Mapping[str, str] = None, + r: Mapping[str, str] = None, + mj: Optional[str] = None, + alpha: Optional[str] = None, + t: Mapping[str, str] = None, + predicate: Optional[AnoncredsGEProofPred] = None, + **kwargs, + ): + """Initialize GE proof object.""" + super().__init__(**kwargs) + self.u = u + self.r = r + self.mj = mj + self.alpha = alpha + self.t = t + self.predicate = predicate + + +class AnoncredsGEProofSchema(BaseModelSchema): + """Anoncreds GE proof schema.""" + + class Meta: + """Anoncreds GE proof schema metadata.""" + + model_class = AnoncredsGEProof + unknown = EXCLUDE + + u = fields.Dict( + keys=fields.Str(), + values=fields.Str( + validate=NUM_STR_WHOLE_VALIDATE, metadata={"example": NUM_STR_WHOLE_EXAMPLE} + ), + ) + r = fields.Dict( + keys=fields.Str(), + values=fields.Str( + validate=NUM_STR_WHOLE_VALIDATE, metadata={"example": NUM_STR_WHOLE_EXAMPLE} + ), + ) + mj = fields.Str( + validate=NUM_STR_WHOLE_VALIDATE, metadata={"example": NUM_STR_WHOLE_EXAMPLE} + ) + alpha = fields.Str( + validate=NUM_STR_WHOLE_VALIDATE, metadata={"example": NUM_STR_WHOLE_EXAMPLE} + ) + t = fields.Dict( + keys=fields.Str(), + values=fields.Str( + validate=NUM_STR_WHOLE_VALIDATE, metadata={"example": NUM_STR_WHOLE_EXAMPLE} + ), + ) + predicate = fields.Nested(AnoncredsGEProofPredSchema) + + +class AnoncredsPrimaryProof(BaseModel): + """Anoncreds primary proof.""" + + class Meta: + """Anoncreds primary proof metadata.""" + + schema_class = "AnoncredsPrimaryProofSchema" + + def __init__( + self, + eq_proof: Optional[AnoncredsEQProof] = None, + ge_proofs: Sequence[AnoncredsGEProof] = None, + **kwargs, + ): + """Initialize anoncreds primary proof.""" + super().__init__(**kwargs) + self.eq_proof = eq_proof + self.ge_proofs = ge_proofs + + +class AnoncredsPrimaryProofSchema(BaseModelSchema): + """Anoncreds primary proof schema.""" + + class Meta: + """Anoncreds primary proof schema metadata.""" + + model_class = AnoncredsPrimaryProof + unknown = EXCLUDE + + eq_proof = fields.Nested( + AnoncredsEQProofSchema, + allow_none=True, + metadata={"description": "Anoncreds equality proof"}, + ) + ge_proofs = fields.Nested( + AnoncredsGEProofSchema, + many=True, + allow_none=True, + metadata={"description": "Anoncreds GE proofs"}, + ) + + +class AnoncredsNonRevocProof(BaseModel): + """Anoncreds non-revocation proof.""" + + class Meta: + """Anoncreds non-revocation proof metadata.""" + + schema_class = "AnoncredsNonRevocProofSchema" + + def __init__( + self, + x_list: Optional[Mapping] = None, + c_list: Optional[Mapping] = None, + **kwargs, + ): + """Initialize anoncreds non-revocation proof.""" + super().__init__(**kwargs) + self.x_list = x_list + self.c_list = c_list + + +class AnoncredsNonRevocProofSchema(BaseModelSchema): + """Anoncreds non-revocation proof schema.""" + + class Meta: + """Anoncreds non-revocation proof schema metadata.""" + + model_class = AnoncredsNonRevocProof + unknown = EXCLUDE + + x_list = fields.Dict(keys=fields.Str(), values=fields.Str()) + c_list = fields.Dict(keys=fields.Str(), values=fields.Str()) + + +class AnoncredsProofProofProofsProof(BaseModel): + """Anoncreds proof.proof.proofs constituent proof.""" + + class Meta: + """Anoncreds proof.proof.proofs constituent proof schema.""" + + schema_class = "AnoncredsProofProofProofsProofSchema" + + def __init__( + self, + primary_proof: Optional[AnoncredsPrimaryProof] = None, + non_revoc_proof: Optional[AnoncredsNonRevocProof] = None, + **kwargs, + ): + """Initialize proof.proof.proofs constituent proof.""" + super().__init__(**kwargs) + self.primary_proof = primary_proof + self.non_revoc_proof = non_revoc_proof + + +class AnoncredsProofProofProofsProofSchema(BaseModelSchema): + """Anoncreds proof.proof.proofs constituent proof schema.""" + + class Meta: + """Anoncreds proof.proof.proofs constituent proof schema metadata.""" + + model_class = AnoncredsProofProofProofsProof + unknown = EXCLUDE + + primary_proof = fields.Nested( + AnoncredsPrimaryProofSchema, metadata={"description": "Anoncreds primary proof"} + ) + non_revoc_proof = fields.Nested( + AnoncredsNonRevocProofSchema, + allow_none=True, + metadata={"description": "Anoncreds non-revocation proof"}, + ) + + +class AnoncredsProofProofAggregatedProof(BaseModel): + """Anoncreds proof.proof aggregated proof.""" + + class Meta: + """Anoncreds proof.proof aggregated proof metadata.""" + + schema_class = "AnoncredsProofProofAggregatedProofSchema" + + def __init__( + self, + c_hash: Optional[str] = None, + c_list: Sequence[Sequence[int]] = None, + **kwargs, + ): + """Initialize anoncreds proof.proof agreggated proof.""" + super().__init__(**kwargs) + self.c_hash = c_hash + self.c_list = c_list + + +class AnoncredsProofProofAggregatedProofSchema(BaseModelSchema): + """Anoncreds proof.proof aggregated proof schema.""" + + class Meta: + """Anoncreds proof.proof aggregated proof schema metadata.""" + + model_class = AnoncredsProofProofAggregatedProof + unknown = EXCLUDE + + c_hash = fields.Str(metadata={"description": "c_hash value"}) + c_list = fields.List( + fields.List(fields.Int(metadata={"strict": True})), + metadata={"description": "c_list value"}, + ) + + +class AnoncredsProofProof(BaseModel): + """Anoncreds proof.proof content.""" + + class Meta: + """Anoncreds proof.proof content metadata.""" + + schema_class = "AnoncredsProofProofSchema" + + def __init__( + self, + proofs: Sequence[AnoncredsProofProofProofsProof] = None, + aggregated_proof: Optional[AnoncredsProofProofAggregatedProof] = None, + **kwargs, + ): + """Initialize anoncreds proof.proof content.""" + super().__init__(**kwargs) + self.proofs = proofs + self.aggregated_proof = aggregated_proof + + +class AnoncredsProofProofSchema(BaseModelSchema): + """Anoncreds proof.proof content schema.""" + + class Meta: + """Anoncreds proof.proof content schema metadata.""" + + model_class = AnoncredsProofProof + unknown = EXCLUDE + + proofs = fields.Nested( + AnoncredsProofProofProofsProofSchema, + many=True, + metadata={"description": "Anoncreds proof proofs"}, + ) + aggregated_proof = fields.Nested( + AnoncredsProofProofAggregatedProofSchema, + metadata={"description": "Anoncreds proof aggregated proof"}, + ) + + +class RawEncoded(BaseModel): + """Raw and encoded attribute values.""" + + class Meta: + """Raw and encoded attribute values metadata.""" + + schema_class = "RawEncodedSchema" + + def __init__( + self, + raw: Optional[str] = None, + encoded: Optional[str] = None, + **kwargs, + ): + """Initialize raw and encoded attribute values.""" + super().__init__(**kwargs) + self.raw = raw + self.encoded = encoded + + +class RawEncodedSchema(BaseModelSchema): + """Raw and encoded attribute values schema.""" + + class Meta: + """Raw and encoded attribute values schema metadata.""" + + model_class = RawEncoded + unknown = EXCLUDE + + raw = fields.Str(metadata={"description": "Raw value"}) + encoded = fields.Str( + validate=NUM_STR_ANY_VALIDATE, + metadata={"description": "Encoded value", "example": NUM_STR_ANY_EXAMPLE}, + ) + + +class AnoncredsProofRequestedProofRevealedAttr(RawEncoded): + """Anoncreds proof requested proof revealed attr.""" + + class Meta: + """Anoncreds proof requested proof revealed attr metadata.""" + + schema_class = "AnoncredsProofRequestedProofRevealedAttrSchema" + + def __init__( + self, + sub_proof_index: Optional[int] = None, + **kwargs, + ): + """Initialize anoncreds proof requested proof revealed attr.""" + super().__init__(**kwargs) + self.sub_proof_index = sub_proof_index + + +class AnoncredsProofRequestedProofRevealedAttrSchema(RawEncodedSchema): + """Anoncreds proof requested proof revealed attr schema.""" + + class Meta: + """Anoncreds proof requested proof revealed attr schema metadata.""" + + model_class = AnoncredsProofRequestedProofRevealedAttr + unknown = EXCLUDE + + sub_proof_index = fields.Int( + metadata={"strict": True, "description": "Sub-proof index"} + ) + + +class AnoncredsProofRequestedProofRevealedAttrGroup(BaseModel): + """Anoncreds proof requested proof revealed attr group.""" + + class Meta: + """Anoncreds proof requested proof revealed attr group metadata.""" + + schema_class = "AnoncredsProofRequestedProofRevealedAttrGroupSchema" + + def __init__( + self, + sub_proof_index: Optional[int] = None, + values: Mapping[str, RawEncoded] = None, + **kwargs, + ): + """Initialize anoncreds proof requested proof revealed attr.""" + super().__init__(**kwargs) + self.sub_proof_index = sub_proof_index + self.values = values + + +class AnoncredsProofRequestedProofRevealedAttrGroupSchema(BaseModelSchema): + """Anoncreds proof requested proof revealed attr group schema.""" + + class Meta: + """Anoncreds proof requested proof revealed attr group schema metadata.""" + + model_class = AnoncredsProofRequestedProofRevealedAttrGroup + unknown = EXCLUDE + + sub_proof_index = fields.Int( + metadata={"strict": True, "description": "Sub-proof index"} + ) + values = fields.Dict( + keys=fields.Str(), + values=fields.Nested(RawEncodedSchema), + metadata={ + "description": "Anoncreds proof requested proof revealed attr groups group value" # noqa: E501 + }, + ) + + +class AnoncredsProofRequestedProofPredicate(BaseModel): + """Anoncreds proof requested proof predicate.""" + + class Meta: + """Anoncreds proof requested proof requested proof predicate metadata.""" + + schema_class = "AnoncredsProofRequestedProofPredicateSchema" + + def __init__( + self, + sub_proof_index: Optional[int] = None, + **kwargs, + ): + """Initialize anoncreds proof requested proof predicate.""" + super().__init__(**kwargs) + self.sub_proof_index = sub_proof_index + + +class AnoncredsProofRequestedProofPredicateSchema(BaseModelSchema): + """Anoncreds proof requested prrof predicate schema.""" + + class Meta: + """Anoncreds proof requested proof requested proof predicate schema metadata.""" + + model_class = AnoncredsProofRequestedProofPredicate + unknown = EXCLUDE + + sub_proof_index = fields.Int( + metadata={"strict": True, "description": "Sub-proof index"} + ) + + +class AnoncredsProofRequestedProof(BaseModel): + """Anoncreds proof.requested_proof content.""" + + class Meta: + """Anoncreds proof.requested_proof content metadata.""" + + schema_class = "AnoncredsProofRequestedProofSchema" + + def __init__( + self, + revealed_attrs: Mapping[str, AnoncredsProofRequestedProofRevealedAttr] = None, + revealed_attr_groups: Mapping[ + str, + AnoncredsProofRequestedProofRevealedAttrGroup, + ] = None, + self_attested_attrs: Optional[Mapping] = None, + unrevealed_attrs: Optional[Mapping] = None, + predicates: Mapping[str, AnoncredsProofRequestedProofPredicate] = None, + **kwargs, + ): + """Initialize anoncreds proof requested proof.""" + super().__init__(**kwargs) + self.revealed_attrs = revealed_attrs + self.revealed_attr_groups = revealed_attr_groups + self.self_attested_attrs = self_attested_attrs + self.unrevealed_attrs = unrevealed_attrs + self.predicates = predicates + + +class AnoncredsProofRequestedProofSchema(BaseModelSchema): + """Anoncreds proof requested proof schema.""" + + class Meta: + """Anoncreds proof requested proof schema metadata.""" + + model_class = AnoncredsProofRequestedProof + unknown = EXCLUDE + + revealed_attrs = fields.Dict( + keys=fields.Str(), + values=fields.Nested(AnoncredsProofRequestedProofRevealedAttrSchema), + allow_none=True, + metadata={"description": "Proof requested proof revealed attributes"}, + ) + revealed_attr_groups = fields.Dict( + keys=fields.Str(), + values=fields.Nested(AnoncredsProofRequestedProofRevealedAttrGroupSchema), + allow_none=True, + metadata={"description": "Proof requested proof revealed attribute groups"}, + ) + self_attested_attrs = fields.Dict( + metadata={"description": "Proof requested proof self-attested attributes"} + ) + unrevealed_attrs = fields.Dict(metadata={"description": "Unrevealed attributes"}) + predicates = fields.Dict( + keys=fields.Str(), + values=fields.Nested(AnoncredsProofRequestedProofPredicateSchema), + metadata={"description": "Proof requested proof predicates."}, + ) + + +class AnoncredsProofIdentifier(BaseModel): + """Anoncreds proof identifier.""" + + class Meta: + """Anoncreds proof identifier metadata.""" + + schema_class = "AnoncredsProofIdentifierSchema" + + def __init__( + self, + schema_id: Optional[str] = None, + cred_def_id: Optional[str] = None, + rev_reg_id: Optional[str] = None, + timestamp: Optional[int] = None, + **kwargs, + ): + """Initialize anoncreds proof identifier.""" + super().__init__(**kwargs) + self.schema_id = schema_id + self.cred_def_id = cred_def_id + self.rev_reg_id = rev_reg_id + self.timestamp = timestamp + + +class AnoncredsProofIdentifierSchema(BaseModelSchema): + """Anoncreds proof identifier schema.""" + + class Meta: + """Anoncreds proof identifier schema metadata.""" + + model_class = AnoncredsProofIdentifier + unknown = EXCLUDE + + schema_id = fields.Str( + validate=ANONCREDS_SCHEMA_ID_VALIDATE, + metadata={ + "description": "Schema identifier", + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, + }, + ) + cred_def_id = fields.Str( + validate=ANONCREDS_CRED_DEF_ID_VALIDATE, + metadata={ + "description": "Credential definition identifier", + "example": ANONCREDS_CRED_DEF_ID_EXAMPLE, + }, + ) + rev_reg_id = fields.Str( + allow_none=True, + validate=ANONCREDS_REV_REG_ID_VALIDATE, + metadata={ + "description": "Revocation registry identifier", + "example": ANONCREDS_REV_REG_ID_EXAMPLE, + }, + ) + timestamp = fields.Int( + allow_none=True, + validate=INT_EPOCH_VALIDATE, + metadata={ + "strict": True, + "description": "Timestamp epoch", + "example": INT_EPOCH_EXAMPLE, + }, + ) + + +class AnoncredsProof(BaseModel): + """Anoncreds proof.""" + + class Meta: + """Anoncreds proof metadata.""" + + schema_class = "AnoncredsProofSchema" + + def __init__( + self, + proof: Optional[AnoncredsProofProof] = None, + requested_proof: Optional[AnoncredsProofRequestedProof] = None, + identifiers: Sequence[AnoncredsProofIdentifier] = None, + **kwargs, + ): + """Initialize anoncreds proof.""" + super().__init__(**kwargs) + self.proof = proof + self.requested_proof = requested_proof + self.identifiers = identifiers + + +class AnoncredsProofSchema(BaseModelSchema): + """Anoncreds proof schema.""" + + class Meta: + """Anoncreds proof schema metadata.""" + + model_class = AnoncredsProof + unknown = EXCLUDE + + proof = fields.Nested( + AnoncredsProofProofSchema, + metadata={"description": "Anoncreds proof.proof content"}, + ) + requested_proof = fields.Nested( + AnoncredsProofRequestedProofSchema, + metadata={"description": "Anoncreds proof.requested_proof content"}, + ) + identifiers = fields.Nested( + AnoncredsProofIdentifierSchema, + many=True, + metadata={"description": "Anoncreds proof.identifiers content"}, + ) + + +class AnoncredsPresSpecSchema(AdminAPIMessageTracingSchema): + """Request schema for anoncreds proof specification to send as presentation.""" + + self_attested_attributes = fields.Dict( + required=True, + keys=fields.Str(metadata={"example": "attr_name"}), + values=fields.Str( + metadata={ + "example": "self_attested_value", + "description": ( + "Self-attested attribute values to use in requested-credentials" + " structure for proof construction" + ), + } + ), + metadata={"description": "Self-attested attributes to build into proof"}, + ) + requested_attributes = fields.Dict( + required=True, + keys=fields.Str(metadata={"example": "attr_referent"}), + values=fields.Nested(AnoncredsRequestedCredsRequestedAttrSchema), + metadata={ + "description": ( + "Nested object mapping proof request attribute referents to" + " requested-attribute specifiers" + ) + }, + ) + requested_predicates = fields.Dict( + required=True, + keys=fields.Str(metadata={"example": "pred_referent"}), + values=fields.Nested(AnoncredsRequestedCredsRequestedPredSchema), + metadata={ + "description": ( + "Nested object mapping proof request predicate referents to" + " requested-predicate specifiers" + ) + }, + ) + trace = fields.Bool( + required=False, + metadata={ + "description": "Whether to trace event (default false)", + "example": False, + }, + ) diff --git a/acapy_agent/anoncreds/models/requested_credentials.py b/acapy_agent/anoncreds/models/requested_credentials.py new file mode 100644 index 0000000000..ac0fcf3d33 --- /dev/null +++ b/acapy_agent/anoncreds/models/requested_credentials.py @@ -0,0 +1,47 @@ +"""Admin routes for presentations.""" + +from marshmallow import fields + +from ...messaging.models.openapi import OpenAPISchema +from ...messaging.valid import INT_EPOCH_EXAMPLE, INT_EPOCH_VALIDATE + + +class AnoncredsRequestedCredsRequestedAttrSchema(OpenAPISchema): + """Schema for requested attributes within anoncreds requested creds structure.""" + + cred_id = fields.Str( + required=True, + metadata={ + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "description": ( + "Wallet credential identifier (typically but not necessarily a UUID)" + ), + }, + ) + revealed = fields.Bool( + dump_default=True, + metadata={"description": "Whether to reveal attribute in proof (default true)"}, + ) + + +class AnoncredsRequestedCredsRequestedPredSchema(OpenAPISchema): + """Schema for requested predicates within anoncreds requested creds structure.""" + + cred_id = fields.Str( + required=True, + metadata={ + "description": ( + "Wallet credential identifier (typically but not necessarily a UUID)" + ), + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + }, + ) + timestamp = fields.Int( + required=False, + validate=INT_EPOCH_VALIDATE, + metadata={ + "description": "Epoch timestamp of interest for non-revocation proof", + "strict": True, + "example": INT_EPOCH_EXAMPLE, + }, + ) diff --git a/acapy_agent/anoncreds/models/anoncreds_revocation.py b/acapy_agent/anoncreds/models/revocation.py similarity index 96% rename from acapy_agent/anoncreds/models/anoncreds_revocation.py rename to acapy_agent/anoncreds/models/revocation.py index cc90469eac..94c75a525a 100644 --- a/acapy_agent/anoncreds/models/anoncreds_revocation.py +++ b/acapy_agent/anoncreds/models/revocation.py @@ -7,15 +7,14 @@ from marshmallow.validate import OneOf from typing_extensions import Literal -from acapy_agent.messaging.valid import ( - INDY_CRED_DEF_ID_EXAMPLE, - INDY_ISO8601_DATETIME_EXAMPLE, - INDY_OR_KEY_DID_EXAMPLE, - INDY_RAW_PUBLIC_KEY_EXAMPLE, - INDY_REV_REG_ID_EXAMPLE, -) - from ...messaging.models.base import BaseModel, BaseModelSchema +from ...messaging.valid import ( + ANONCREDS_CRED_DEF_ID_EXAMPLE, + ANONCREDS_DID_EXAMPLE, + ANONCREDS_REV_REG_ID_EXAMPLE, + ISO8601_DATETIME_EXAMPLE, + RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, +) class RevRegDefValue(BaseModel): @@ -60,7 +59,7 @@ class Meta: unknown = EXCLUDE public_keys = fields.Dict( - data_key="publicKeys", metadata={"example": INDY_RAW_PUBLIC_KEY_EXAMPLE} + data_key="publicKeys", metadata={"example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE} ) max_cred_num = fields.Int(data_key="maxCredNum", metadata={"example": 777}) tails_location = fields.Str( @@ -131,7 +130,7 @@ class Meta: issuer_id = fields.Str( metadata={ "description": "Issuer Identifier of the credential definition or schema", - "example": INDY_OR_KEY_DID_EXAMPLE, + "example": ANONCREDS_DID_EXAMPLE, }, data_key="issuerId", ) @@ -139,7 +138,7 @@ class Meta: cred_def_id = fields.Str( metadata={ "description": "Credential definition identifier", - "example": INDY_CRED_DEF_ID_EXAMPLE, + "example": ANONCREDS_CRED_DEF_ID_EXAMPLE, }, data_key="credDefId", ) @@ -209,7 +208,7 @@ class Meta: revocation_registry_definition_id = fields.Str( metadata={ "description": "revocation registry definition id", - "example": INDY_REV_REG_ID_EXAMPLE, + "example": ANONCREDS_REV_REG_ID_EXAMPLE, } ) revocation_registry_definition = fields.Nested( @@ -380,14 +379,14 @@ class Meta: issuer_id = fields.Str( metadata={ "description": "Issuer Identifier of the credential definition or schema", - "example": INDY_OR_KEY_DID_EXAMPLE, + "example": ANONCREDS_DID_EXAMPLE, }, data_key="issuerId", ) rev_reg_def_id = fields.Str( metadata={ "description": "The ID of the revocation registry definition", - "example": INDY_REV_REG_ID_EXAMPLE, + "example": ANONCREDS_REV_REG_ID_EXAMPLE, }, data_key="revRegDefId", ) @@ -409,7 +408,7 @@ class Meta: timestamp = fields.Int( metadata={ "description": "Timestamp at which revocation list is applicable", - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, required=False, ) diff --git a/acapy_agent/anoncreds/models/anoncreds_schema.py b/acapy_agent/anoncreds/models/schema.py similarity index 95% rename from acapy_agent/anoncreds/models/anoncreds_schema.py rename to acapy_agent/anoncreds/models/schema.py index b2383ff60f..6a3f56bc03 100644 --- a/acapy_agent/anoncreds/models/anoncreds_schema.py +++ b/acapy_agent/anoncreds/models/schema.py @@ -7,7 +7,10 @@ from marshmallow.validate import OneOf from ...messaging.models.base import BaseModel, BaseModelSchema -from ...messaging.valid import INDY_OR_KEY_DID_EXAMPLE, INDY_SCHEMA_ID_EXAMPLE +from ...messaging.valid import ( + ANONCREDS_DID_EXAMPLE, + ANONCREDS_SCHEMA_ID_EXAMPLE, +) class AnonCredsSchema(BaseModel): @@ -58,7 +61,7 @@ class Meta: issuer_id = fields.Str( metadata={ "description": "Issuer Identifier of the credential definition or schema", - "example": INDY_OR_KEY_DID_EXAMPLE, + "example": ANONCREDS_DID_EXAMPLE, }, data_key="issuerId", ) @@ -129,7 +132,10 @@ class Meta: schema_value = fields.Nested(AnonCredsSchemaSchema(), data_key="schema") schema_id = fields.Str( - metadata={"description": "Schema identifier", "example": INDY_SCHEMA_ID_EXAMPLE} + metadata={ + "description": "Schema identifier", + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, + } ) resolution_metadata = fields.Dict() schema_metadata = fields.Dict() @@ -185,7 +191,7 @@ class Meta: schema_id = fields.Str( metadata={ "description": "Schema identifier", - "example": INDY_SCHEMA_ID_EXAMPLE, + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, } ) schema_value = fields.Nested(AnonCredsSchemaSchema(), data_key="schema") diff --git a/acapy_agent/anoncreds/models/utils.py b/acapy_agent/anoncreds/models/utils.py new file mode 100644 index 0000000000..51f140844f --- /dev/null +++ b/acapy_agent/anoncreds/models/utils.py @@ -0,0 +1,99 @@ +"""Utilities to deal with anoncreds objects.""" + +from ..holder import AnonCredsHolder + + +def _get_value_error_msg(proof_request: dict, referent: str) -> str: + return ( + "Could not automatically construct presentation for " + + f"presentation request {proof_request['name']}" + + f":{proof_request['version']} because referent " + + f"{referent} did not produce any credentials." + ) + + +async def get_requested_creds_from_proof_request_preview( + proof_request: dict, + *, + holder: AnonCredsHolder, +): + """Build anoncreds requested-credentials structure. + + Given input proof request and presentation preview, use credentials in + holder's wallet to build anoncreds requested credentials structure for input + to proof creation. + + Args: + proof_request: anoncreds proof request + preview: preview from presentation proposal, if applicable + holder: holder injected into current context + + """ + req_creds = { + "self_attested_attributes": {}, + "requested_attributes": {}, + "requested_predicates": {}, + } + + for referent, _ in proof_request["requested_attributes"].items(): + credentials = await holder.get_credentials_for_presentation_request_by_referent( + presentation_request=proof_request, + referents=(referent,), + start=0, + count=100, + ) + if not credentials: + raise ValueError(_get_value_error_msg(proof_request, referent)) + + cred_match = credentials[0] # holder sorts + + if "restrictions" in proof_request["requested_attributes"][referent]: + req_creds["requested_attributes"][referent] = { + "cred_id": cred_match["cred_info"]["referent"], + "revealed": True, + } + else: + req_creds["self_attested_attributes"][referent] = cred_match["cred_info"][ + "attrs" + ][proof_request["requested_attributes"][referent]["name"]] + + for referent in proof_request["requested_predicates"]: + credentials = await holder.get_credentials_for_presentation_request_by_referent( + presentation_request=proof_request, + referents=(referent,), + start=0, + count=100, + ) + if not credentials: + raise ValueError(_get_value_error_msg(proof_request, referent)) + + cred_match = credentials[0] # holder sorts + if "restrictions" in proof_request["requested_predicates"][referent]: + req_creds["requested_predicates"][referent] = { + "cred_id": cred_match["cred_info"]["referent"], + "revealed": True, + } + else: + req_creds["self_attested_attributes"][referent] = cred_match["cred_info"][ + "attrs" + ][proof_request["requested_predicates"][referent]["name"]] + + return req_creds + + +def extract_non_revocation_intervals_from_proof_request(proof_req: dict): + """Return non-revocation intervals by requested item referent in proof request.""" + non_revoc_intervals = {} + for req_item_type in ("requested_attributes", "requested_predicates"): + for reft, req_item in proof_req[req_item_type].items(): + interval = req_item.get( + "non_revoked", + proof_req.get("non_revoked"), + ) + if interval: + timestamp_from = interval.get("from") + timestamp_to = interval.get("to") + if (timestamp_to is not None) and timestamp_from == timestamp_to: + interval["from"] = 0 # accommodate verify=False if from=to + non_revoc_intervals[reft] = interval + return non_revoc_intervals diff --git a/acapy_agent/anoncreds/registry.py b/acapy_agent/anoncreds/registry.py index 79aba036cc..c355b447cd 100644 --- a/acapy_agent/anoncreds/registry.py +++ b/acapy_agent/anoncreds/registry.py @@ -11,8 +11,8 @@ BaseAnonCredsRegistrar, BaseAnonCredsResolver, ) -from .models.anoncreds_cred_def import CredDef, CredDefResult, GetCredDefResult -from .models.anoncreds_revocation import ( +from .models.credential_definition import CredDef, CredDefResult, GetCredDefResult +from .models.revocation import ( GetRevListResult, GetRevRegDefResult, RevList, @@ -20,7 +20,7 @@ RevRegDef, RevRegDefResult, ) -from .models.anoncreds_schema import AnonCredsSchema, GetSchemaResult, SchemaResult +from .models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult LOGGER = logging.getLogger(__name__) diff --git a/acapy_agent/anoncreds/revocation.py b/acapy_agent/anoncreds/revocation.py index 56b19d222d..e9cc88293f 100644 --- a/acapy_agent/anoncreds/revocation.py +++ b/acapy_agent/anoncreds/revocation.py @@ -26,12 +26,12 @@ from requests import RequestException, Session from uuid_utils import uuid4 -from acapy_agent.anoncreds.models.anoncreds_cred_def import CredDef - from ..askar.profile_anon import AskarAnoncredsProfile, AskarAnoncredsProfileSession from ..core.error import BaseError from ..core.event_bus import Event, EventBus from ..core.profile import Profile, ProfileSession +from ..multitenant.base import BaseMultitenantManager +from ..tails.anoncreds_tails_server import AnonCredsTailsServer from ..tails.base import BaseTailsServer from .error_messages import ANONCREDS_PROFILE_REQUIRED_MSG from .events import RevListFinishedEvent, RevRegDefFinishedEvent @@ -41,7 +41,8 @@ STATE_FINISHED, AnonCredsIssuer, ) -from .models.anoncreds_revocation import ( +from .models.credential_definition import CredDef +from .models.revocation import ( RevList, RevListResult, RevListState, @@ -694,7 +695,12 @@ def get_local_tails_path(self, rev_reg_def: RevRegDef) -> str: async def upload_tails_file(self, rev_reg_def: RevRegDef): """Upload the local tails file to the tails server.""" - tails_server = self.profile.inject_or(BaseTailsServer) + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + tails_server = AnonCredsTailsServer() + else: + tails_server = self.profile.inject_or(BaseTailsServer) + if not tails_server: raise AnonCredsRevocationError("Tails server not configured") if not Path(self.get_local_tails_path(rev_reg_def)).is_file(): diff --git a/acapy_agent/anoncreds/routes.py b/acapy_agent/anoncreds/routes.py index 9cf1d6ad98..9a8fe04669 100644 --- a/acapy_agent/anoncreds/routes.py +++ b/acapy_agent/anoncreds/routes.py @@ -16,13 +16,12 @@ from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..core.event_bus import EventBus -from ..ledger.error import LedgerError from ..messaging.models.openapi import OpenAPISchema from ..messaging.valid import ( - INDY_CRED_DEF_ID_EXAMPLE, - INDY_OR_KEY_DID_EXAMPLE, - INDY_REV_REG_ID_EXAMPLE, - INDY_SCHEMA_ID_EXAMPLE, + ANONCREDS_CRED_DEF_ID_EXAMPLE, + ANONCREDS_DID_EXAMPLE, + ANONCREDS_REV_REG_ID_EXAMPLE, + ANONCREDS_SCHEMA_ID_EXAMPLE, UUIDFour, ) from ..revocation.error import RevocationNotSupportedError @@ -35,9 +34,9 @@ AnonCredsResolutionError, ) from .issuer import AnonCredsIssuer, AnonCredsIssuerError -from .models.anoncreds_cred_def import CredDefResultSchema, GetCredDefResultSchema -from .models.anoncreds_revocation import RevListResultSchema, RevRegDefResultSchema -from .models.anoncreds_schema import ( +from .models.credential_definition import CredDefResultSchema, GetCredDefResultSchema +from .models.revocation import RevListResultSchema, RevRegDefResultSchema +from .models.schema import ( AnonCredsSchemaSchema, GetSchemaResultSchema, SchemaResultSchema, @@ -69,7 +68,7 @@ class SchemaIdMatchInfo(OpenAPISchema): schema_id = fields.Str( metadata={ "description": "Schema identifier", - "example": INDY_SCHEMA_ID_EXAMPLE, + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, } ) @@ -112,7 +111,7 @@ class SchemasQueryStringSchema(OpenAPISchema): schema_issuer_id = fields.Str( metadata={ "description": "Schema issuer identifier", - "example": INDY_OR_KEY_DID_EXAMPLE, + "example": ANONCREDS_DID_EXAMPLE, } ) @@ -124,7 +123,7 @@ class GetSchemasResponseSchema(OpenAPISchema): fields.Str( metadata={ "description": "Schema identifiers", - "example": INDY_SCHEMA_ID_EXAMPLE, + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, } ) ) @@ -137,7 +136,7 @@ class SchemaPostRequestSchema(OpenAPISchema): options = fields.Nested(SchemaPostOptionSchema()) -@docs(tags=["anoncreds - schemas"], summary="Create a schema on the connected ledger") +@docs(tags=["anoncreds - schemas"], summary="Create a schema on the connected datastore") @request_schema(SchemaPostRequestSchema()) @response_schema(SchemaResultSchema(), 200, description="") @tenant_authentication @@ -278,7 +277,7 @@ class CredIdMatchInfo(OpenAPISchema): cred_def_id = fields.Str( metadata={ "description": "Credential definition identifier", - "example": INDY_CRED_DEF_ID_EXAMPLE, + "example": ANONCREDS_CRED_DEF_ID_EXAMPLE, }, required=True, ) @@ -297,7 +296,7 @@ class InnerCredDefSchema(OpenAPISchema): schema_id = fields.Str( metadata={ "description": "Schema identifier", - "example": INDY_SCHEMA_ID_EXAMPLE, + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, }, required=True, data_key="schemaId", @@ -305,7 +304,7 @@ class InnerCredDefSchema(OpenAPISchema): issuer_id = fields.Str( metadata={ "description": "Issuer Identifier of the credential definition", - "example": INDY_OR_KEY_DID_EXAMPLE, + "example": ANONCREDS_DID_EXAMPLE, }, required=True, data_key="issuerId", @@ -357,13 +356,13 @@ class CredDefsQueryStringSchema(OpenAPISchema): issuer_id = fields.Str( metadata={ "description": "Issuer Identifier of the credential definition", - "example": INDY_OR_KEY_DID_EXAMPLE, + "example": ANONCREDS_DID_EXAMPLE, } ) schema_id = fields.Str( metadata={ "description": "Schema identifier", - "example": INDY_SCHEMA_ID_EXAMPLE, + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, } ) schema_name = fields.Str( @@ -382,7 +381,7 @@ class CredDefsQueryStringSchema(OpenAPISchema): @docs( tags=["anoncreds - credential definitions"], - summary="Create a credential definition on the connected ledger", + summary="Create a credential definition on the connected datastore", ) @request_schema(CredDefPostRequestSchema()) @response_schema(CredDefResultSchema(), 200, description="") @@ -522,14 +521,14 @@ class InnerRevRegDefSchema(OpenAPISchema): issuer_id = fields.Str( metadata={ "description": "Issuer Identifier of the credential definition or schema", - "example": INDY_OR_KEY_DID_EXAMPLE, + "example": ANONCREDS_DID_EXAMPLE, }, data_key="issuerId", ) cred_def_id = fields.Str( metadata={ "description": "Credential definition identifier", - "example": INDY_SCHEMA_ID_EXAMPLE, + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, }, data_key="credDefId", ) @@ -573,7 +572,7 @@ class RevRegCreateRequestSchemaAnoncreds(OpenAPISchema): @docs( tags=["anoncreds - revocation"], - summary="Create and publish a registration revocation on the connected ledger", + summary="Create and publish a registration revocation on the connected datastore", ) @request_schema(RevRegCreateRequestSchemaAnoncreds()) @response_schema(RevRegDefResultSchema(), 200, description="") @@ -649,7 +648,7 @@ class RevListCreateRequestSchema(OpenAPISchema): rev_reg_def_id = fields.Str( metadata={ "description": "Revocation registry definition identifier", - "example": INDY_REV_REG_ID_EXAMPLE, + "example": ANONCREDS_REV_REG_ID_EXAMPLE, } ) options = fields.Nested(RevListOptionsSchema) @@ -657,7 +656,7 @@ class RevListCreateRequestSchema(OpenAPISchema): @docs( tags=["anoncreds - revocation"], - summary="Create and publish a revocation status list on the connected ledger", + summary="Create and publish a revocation status list on the connected datastore", ) @request_schema(RevListCreateRequestSchema()) @response_schema(RevListResultSchema(), 200, description="") @@ -687,7 +686,7 @@ async def rev_list_post(request: web.BaseRequest): handle_value_error(e) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - except (AnonCredsRevocationError, LedgerError) as err: + except AnonCredsRevocationError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err diff --git a/acapy_agent/anoncreds/tests/test_holder.py b/acapy_agent/anoncreds/tests/test_holder.py index ba69a7ebd7..34d23ef75f 100644 --- a/acapy_agent/anoncreds/tests/test_holder.py +++ b/acapy_agent/anoncreds/tests/test_holder.py @@ -45,8 +45,8 @@ from ...wallet.error import WalletNotFoundError from .. import holder as test_module from ..holder import CATEGORY_CREDENTIAL, AnonCredsHolder, AnonCredsHolderError -from ..models.anoncreds_cred_def import CredDef, CredDefValue, CredDefValuePrimary -from ..models.anoncreds_revocation import GetRevListResult, RevList +from ..models.credential_definition import CredDef, CredDefValue, CredDefValuePrimary +from ..models.revocation import GetRevListResult, RevList from ..registry import AnonCredsRegistry diff --git a/acapy_agent/anoncreds/tests/test_issuer.py b/acapy_agent/anoncreds/tests/test_issuer.py index f165f178d6..7442ffd2ae 100644 --- a/acapy_agent/anoncreds/tests/test_issuer.py +++ b/acapy_agent/anoncreds/tests/test_issuer.py @@ -10,7 +10,7 @@ AnonCredsObjectAlreadyExists, AnonCredsSchemaAlreadyExists, ) -from ...anoncreds.models.anoncreds_cred_def import ( +from ...anoncreds.models.credential_definition import ( CredDef, CredDefResult, CredDefState, @@ -19,7 +19,7 @@ CredDefValueRevocation, GetCredDefResult, ) -from ...anoncreds.models.anoncreds_schema import ( +from ...anoncreds.models.schema import ( AnonCredsSchema, GetSchemaResult, SchemaResult, diff --git a/acapy_agent/anoncreds/tests/test_revocation.py b/acapy_agent/anoncreds/tests/test_revocation.py index 5de4ef368e..2d9bc62e15 100644 --- a/acapy_agent/anoncreds/tests/test_revocation.py +++ b/acapy_agent/anoncreds/tests/test_revocation.py @@ -16,8 +16,8 @@ from requests import RequestException, Session from ...anoncreds.issuer import AnonCredsIssuer -from ...anoncreds.models.anoncreds_cred_def import CredDef -from ...anoncreds.models.anoncreds_revocation import ( +from ...anoncreds.models.credential_definition import CredDef +from ...anoncreds.models.revocation import ( RevList, RevListResult, RevListState, @@ -26,7 +26,7 @@ RevRegDefState, RevRegDefValue, ) -from ...anoncreds.models.anoncreds_schema import ( +from ...anoncreds.models.schema import ( AnonCredsSchema, GetSchemaResult, ) @@ -48,7 +48,7 @@ "accum_key": {"z": "1 0BB...386"}, }, tails_hash="58NNWYnVxVFzAfUztwGSNBL4551XNq6nXk56pCiKJxxt", - tails_location="http://tails-server.com", + tails_location="https://tails-server.com", ), issuer_id="CsQY9MGeD3CQP4EyuVFo5m", type="CL_ACCUM", @@ -836,21 +836,40 @@ def test_generate_public_tails_uri(self): async def test_upload_tails_file(self): self.profile.inject_or = mock.Mock( - return_value=mock.MagicMock( - upload_tails_file=mock.CoroutineMock( - side_effect=[ - (True, "http://tails-server.com"), - (None, "http://tails-server.com"), - (True, "not-http://tails-server.com"), - ] - ) - ) + side_effect=[ + None, + mock.MagicMock( + upload_tails_file=mock.CoroutineMock( + return_value=(True, "https://tails-server.com") + ) + ), + ] ) # valid await self.revocation.upload_tails_file(rev_reg_def) # upload fails + self.profile.inject_or = mock.Mock( + side_effect=[ + None, + mock.MagicMock( + upload_tails_file=mock.CoroutineMock( + return_value=(None, "https://tails-server.com"), + ) + ), + ] + ) with self.assertRaises(test_module.AnonCredsRevocationError): await self.revocation.upload_tails_file(rev_reg_def) + self.profile.inject_or = mock.Mock( + side_effect=[ + None, + mock.MagicMock( + upload_tails_file=mock.CoroutineMock( + return_value=(True, "not-http://tails-server.com"), + ) + ), + ] + ) # tails location does not match with self.assertRaises(test_module.AnonCredsRevocationError): await self.revocation.upload_tails_file(rev_reg_def) diff --git a/acapy_agent/anoncreds/tests/test_revocation_setup.py b/acapy_agent/anoncreds/tests/test_revocation_setup.py index e56291efeb..357899f4c9 100644 --- a/acapy_agent/anoncreds/tests/test_revocation_setup.py +++ b/acapy_agent/anoncreds/tests/test_revocation_setup.py @@ -11,7 +11,7 @@ RevRegDefFinishedEvent, RevRegDefFinishedPayload, ) -from ..models.anoncreds_revocation import RevRegDef, RevRegDefValue +from ..models.revocation import RevRegDef, RevRegDefValue from ..revocation import AnonCredsRevocation diff --git a/acapy_agent/anoncreds/tests/test_routes.py b/acapy_agent/anoncreds/tests/test_routes.py index 17c4d15bee..607b81f4ae 100644 --- a/acapy_agent/anoncreds/tests/test_routes.py +++ b/acapy_agent/anoncreds/tests/test_routes.py @@ -7,7 +7,7 @@ from ...admin.request_context import AdminRequestContext from ...anoncreds.base import AnonCredsObjectNotFound from ...anoncreds.issuer import AnonCredsIssuer -from ...anoncreds.models.anoncreds_schema import ( +from ...anoncreds.models.schema import ( AnonCredsSchema, SchemaResult, SchemaState, diff --git a/acapy_agent/anoncreds/tests/test_verifier.py b/acapy_agent/anoncreds/tests/test_verifier.py index 8f912a3f0f..0ff11fcb95 100644 --- a/acapy_agent/anoncreds/tests/test_verifier.py +++ b/acapy_agent/anoncreds/tests/test_verifier.py @@ -3,21 +3,21 @@ import pytest -from ...anoncreds.models.anoncreds_cred_def import ( +from ...anoncreds.models.credential_definition import ( CredDef, CredDefValue, CredDefValuePrimary, CredDefValueRevocation, GetCredDefResult, ) -from ...anoncreds.models.anoncreds_revocation import ( +from ...anoncreds.models.revocation import ( GetRevListResult, GetRevRegDefResult, RevList, RevRegDef, RevRegDefValue, ) -from ...anoncreds.models.anoncreds_schema import ( +from ...anoncreds.models.schema import ( AnonCredsSchema, GetSchemaResult, ) diff --git a/acapy_agent/anoncreds/verifier.py b/acapy_agent/anoncreds/verifier.py index 4f7972371d..849c44ab5b 100644 --- a/acapy_agent/anoncreds/verifier.py +++ b/acapy_agent/anoncreds/verifier.py @@ -1,4 +1,4 @@ -"""Indy-Credx verifier implementation.""" +"""Anoncreds verifier implementation.""" import asyncio import logging @@ -9,10 +9,10 @@ from anoncreds import AnoncredsError, Presentation, W3cPresentation from ..core.profile import Profile -from ..indy.models.xform import indy_proof_req2non_revoc_intervals from ..messaging.util import canon, encode from ..vc.vc_ld.validation_result import PresentationVerificationResult -from .models.anoncreds_cred_def import GetCredDefResult +from .models.credential_definition import GetCredDefResult +from .models.utils import extract_non_revocation_intervals_from_proof_request from .registry import AnonCredsRegistry LOGGER = logging.getLogger(__name__) @@ -45,7 +45,7 @@ def non_revoc_intervals(self, pres_req: dict, pres: dict, cred_defs: dict) -> li """Remove superfluous non-revocation intervals in presentation request. Irrevocable credentials constitute proof of non-revocation, but - indy rejects proof requests with non-revocation intervals lining up + anoncreds rejects proof requests with non-revocation intervals lining up with non-revocable credentials in proof: seek and remove. Args: @@ -116,13 +116,15 @@ async def check_timestamps( Args: profile: relevant profile - pres_req: indy proof request - pres: indy proof request + pres_req: anoncreds proof request + pres: anoncreds proof request rev_reg_defs: rev reg defs by rev reg id, augmented with transaction times """ msgs = [] now = int(time()) - non_revoc_intervals = indy_proof_req2non_revoc_intervals(pres_req) + non_revoc_intervals = extract_non_revocation_intervals_from_proof_request( + pres_req + ) LOGGER.debug(f">>> got non-revoc intervals: {non_revoc_intervals}") # timestamp for irrevocable credential diff --git a/acapy_agent/connections/models/conn_record.py b/acapy_agent/connections/models/conn_record.py index 874d8c3d7f..e27be92c4f 100644 --- a/acapy_agent/connections/models/conn_record.py +++ b/acapy_agent/connections/models/conn_record.py @@ -11,8 +11,8 @@ from ...messaging.valid import ( GENERIC_DID_EXAMPLE, GENERIC_DID_VALIDATE, - INDY_RAW_PUBLIC_KEY_EXAMPLE, - INDY_RAW_PUBLIC_KEY_VALIDATE, + RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, UUID4_EXAMPLE, ) from ...protocols.connections.v1_0.message_types import ARIES_PROTOCOL as CONN_PROTO @@ -725,10 +725,10 @@ class Meta: ) invitation_key = fields.Str( required=False, - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Public key for connection", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ) invitation_msg_id = fields.Str( diff --git a/acapy_agent/connections/models/connection_target.py b/acapy_agent/connections/models/connection_target.py index ad7219db0a..bc81adc42e 100644 --- a/acapy_agent/connections/models/connection_target.py +++ b/acapy_agent/connections/models/connection_target.py @@ -8,8 +8,8 @@ from ...messaging.valid import ( GENERIC_DID_EXAMPLE, GENERIC_DID_VALIDATE, - INDY_RAW_PUBLIC_KEY_EXAMPLE, - INDY_RAW_PUBLIC_KEY_VALIDATE, + RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, ) @@ -75,10 +75,10 @@ class Meta: ) recipient_keys = fields.List( fields.Str( - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Recipient public key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ), required=False, @@ -86,10 +86,10 @@ class Meta: ) routing_keys = fields.List( fields.Str( - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Routing key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ), data_key="routingKeys", @@ -98,9 +98,9 @@ class Meta: ) sender_key = fields.Str( required=False, - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Sender public key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ) diff --git a/acapy_agent/did/cheqd/error_messages.py b/acapy_agent/did/cheqd/error_messages.py new file mode 100644 index 0000000000..7f2edfe416 --- /dev/null +++ b/acapy_agent/did/cheqd/error_messages.py @@ -0,0 +1,3 @@ +"""Error messages for did cheqd.""" + +DID_CHEQD_REQUIRED_MSG = "AnonCreds interface requires AskarAnoncreds profile" diff --git a/acapy_agent/did/cheqd/cheqd_manager.py b/acapy_agent/did/cheqd/manager.py similarity index 79% rename from acapy_agent/did/cheqd/cheqd_manager.py rename to acapy_agent/did/cheqd/manager.py index b094909b22..30a694b2a9 100644 --- a/acapy_agent/did/cheqd/cheqd_manager.py +++ b/acapy_agent/did/cheqd/manager.py @@ -3,9 +3,9 @@ import logging from aiohttp import web -from aiohttp.web_response import Response from .registrar import DidCheqdRegistrar +from ...core.error import BaseError from ...core.profile import Profile from ...resolver.base import DIDNotFound from ...resolver.default.cheqd import CheqdDIDResolver @@ -65,7 +65,7 @@ async def sign_requests(wallet: BaseWallet, signing_requests): return signed_responses - async def create(self, options: dict) -> Response: + async def create(self, options: dict) -> dict: """Register a Cheqd DID.""" options = options or {} @@ -85,7 +85,7 @@ async def create(self, options: dict) -> Response: try: wallet = session.inject(BaseWallet) if not wallet: - raise web.HTTPForbidden(reason="No wallet available") + raise WalletError(reason="No wallet available") key = await wallet.create_key(key_type, seed) verkey = key.verkey @@ -97,7 +97,7 @@ async def create(self, options: dict) -> Response: network, public_key_hex ) if generate_res is None: - raise WalletError("Error constructing DID Document") + raise DIDCheqdManagerError("Error constructing DID Document") did_document = generate_res.get("didDoc") did: str = did_document.get("id") @@ -112,7 +112,9 @@ async def create(self, options: dict) -> Response: if did_state.get("state") == "action": signing_requests: dict = did_state.get("signingRequest") if not signing_requests: - raise WalletError("No signing requests available for create.") + raise DIDCheqdManagerError( + "No signing requests available for create." + ) # Note: This assumes did create operation supports only one did kid: str = signing_requests[0].get("kid") @@ -133,29 +135,26 @@ async def create(self, options: dict) -> Response: ) publish_did_state = publish_did_res.get("didState") if publish_did_state.get("state") != "finished": - raise WalletError( + raise DIDCheqdManagerError( f"Error registering DID {publish_did_state.get("reason")}" ) else: - raise WalletError(f"Error registering DID {did_state.get("reason")}") + raise DIDCheqdManagerError( + f"Error registering DID {did_state.get("reason")}" + ) # create public did record await wallet.create_public_did(CHEQD, key_type, seed, did) await wallet.assign_kid_to_key(verkey, kid) - except WalletError as err: - return web.json_response({"error": f"Wallet Error: {err}"}, status=400) - except Exception as e: - return web.json_response( - {"error": f"An unexpected error occurred: {str(e)}"}, status=500 - ) - return web.json_response( - { - "did": did, - "verkey": verkey, - } - ) - - async def update(self, did: str, didDoc: dict, options: dict) -> Response: + except Exception as ex: + raise ex + return { + "did": did, + "verkey": verkey, + "didDocument": publish_did_state.get("didDocument"), + } + + async def update(self, did: str, didDoc: dict, _options: dict) -> dict: """Update a Cheqd DID.""" async with self.profile.session() as session: @@ -186,7 +185,7 @@ async def update(self, did: str, didDoc: dict, options: dict) -> Response: if did_state.get("state") == "action": signing_requests: dict = did_state.get("signingRequest") if not signing_requests: - raise WalletError("No signing requests available for update.") + raise Exception("No signing requests available for update.") # sign all requests signed_responses = await DidCheqdManager.sign_requests( wallet, signing_requests @@ -204,29 +203,21 @@ async def update(self, did: str, didDoc: dict, options: dict) -> Response: publish_did_state = publish_did_res.get("didState") if publish_did_state.get("state") != "finished": - raise WalletError( + raise DIDCheqdManagerError( f"Error publishing DID \ update {publish_did_state.get("description")}" ) else: - raise WalletError(f"Error updating DID {did_state.get("reason")}") + raise DIDCheqdManagerError( + f"Error updating DID {did_state.get("reason")}" + ) # TODO update new keys to wallet if necessary - except WalletError as err: - return web.json_response({"error": f"Wallet Error: {err}"}, status=400) - except DIDNotFound as err: - return web.json_response({"error": f"DID Not Found: {err}"}, status=404) - except Exception as e: - return web.json_response( - {"error": f"An unexpected error occurred: {str(e)}"}, status=500 - ) - return web.json_response( - { - "did": did, - "did_state": publish_did_state.get("state"), - } - ) - - async def deactivate(self, did: str) -> Response: + except Exception as ex: + raise ex + + return {"did": did, "didDocument": publish_did_state.get("didDocument")} + + async def deactivate(self, did: str) -> dict: """Deactivate a Cheqd DID.""" LOGGER.debug("Deactivate did: %s", did) @@ -277,18 +268,14 @@ async def deactivate(self, did: str) -> Response: did_info = await wallet.get_local_did(did) metadata = {**did_info.metadata, "deactivated": True} await wallet.replace_local_did_metadata(did, metadata) - except WalletError as err: - return web.json_response({"error": f"Wallet Error: {err}"}, status=400) - except DIDNotFound as err: - return web.json_response({"error": f"DID Not Found: {err}"}, status=404) - except Exception as e: - return web.json_response( - {"error": f"An unexpected error occurred: {str(e)}"}, status=500 - ) - return web.json_response( - { - "did": did, - "did_document": publish_did_state.get("didDocument"), - "did_document_metadata": metadata, - } - ) + except Exception as ex: + raise ex + return { + "did": did, + "did_document": publish_did_state.get("didDocument"), + "did_document_metadata": metadata, + } + + +class DIDCheqdManagerError(BaseError): + """Base class for did cheqd manager exceptions.""" diff --git a/acapy_agent/did/cheqd/routes.py b/acapy_agent/did/cheqd/routes.py index b1537662ba..2b8970ea13 100644 --- a/acapy_agent/did/cheqd/routes.py +++ b/acapy_agent/did/cheqd/routes.py @@ -6,9 +6,10 @@ from aiohttp_apispec import docs, request_schema, response_schema from marshmallow import Schema, fields +from .manager import DIDCheqdManagerError from ...admin.decorators.auth import tenant_authentication from ...admin.request_context import AdminRequestContext -from ...did.cheqd.cheqd_manager import DidCheqdManager +from ...did.cheqd.manager import DidCheqdManager from ...messaging.models.openapi import OpenAPISchema from ...messaging.valid import CHEQD_DID_EXAMPLE, CHEQD_DID_VALIDATE from ...wallet.error import WalletError @@ -259,9 +260,14 @@ async def create_cheqd_did(request: web.BaseRequest): body = {} try: - return await DidCheqdManager(context.profile).create(body.get("options")) - except WalletError as e: - raise web.HTTPBadRequest(reason=str(e)) + result = await DidCheqdManager(context.profile).create(body.get("options")) + return web.json_response( + {"did": result.get("did"), "verkey": result.get("verkey")} + ) + except DIDCheqdManagerError as err: + raise web.HTTPInternalServerError(reason=err.roll_up) + except WalletError as err: + raise web.HTTPBadRequest(reason=err.roll_up) @docs(tags=["did"], summary="Update a did:cheqd") @@ -278,14 +284,16 @@ async def update_cheqd_did(request: web.BaseRequest): body = {} try: - return await DidCheqdManager(context.profile).update( + result = await DidCheqdManager(context.profile).update( body.get("did"), body.get("didDocument"), body.get("options"), ) - - except WalletError as e: - raise web.HTTPBadRequest(reason=str(e)) + return web.json_response(result) + except DIDCheqdManagerError as err: + raise web.HTTPInternalServerError(reason=err.roll_up) + except WalletError as err: + raise web.HTTPBadRequest(reason=err.roll_up) @docs(tags=["did"], summary="Deactivate a did:cheqd") @@ -302,9 +310,12 @@ async def deactivate_cheqd_did(request: web.BaseRequest): body = {} try: - return await DidCheqdManager(context.profile).deactivate(body.get("did")) - except WalletError as e: - raise web.HTTPBadRequest(reason=str(e)) + result = await DidCheqdManager(context.profile).deactivate(body.get("did")) + return web.json_response(result) + except DIDCheqdManagerError as err: + raise web.HTTPInternalServerError(reason=err.roll_up) + except WalletError as err: + raise web.HTTPBadRequest(reason=err.roll_up) async def register(app: web.Application): @@ -314,7 +325,6 @@ async def register(app: web.Application): web.post("/did/cheqd/create", create_cheqd_did), web.post("/did/cheqd/update", update_cheqd_did), web.post("/did/cheqd/deactivate", deactivate_cheqd_did), - web.post("/did/cheqd/key"), ] ) diff --git a/acapy_agent/did/cheqd/tests/__init__.py b/acapy_agent/did/cheqd/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/acapy_agent/did/cheqd/tests/test_manager.py b/acapy_agent/did/cheqd/tests/test_manager.py new file mode 100644 index 0000000000..fe157acc80 --- /dev/null +++ b/acapy_agent/did/cheqd/tests/test_manager.py @@ -0,0 +1,30 @@ +from unittest.async_case import IsolatedAsyncioTestCase + +import logging +import pytest +from acapy_agent.utils.testing import create_test_profile +from ..manager import DidCheqdManager +from ....wallet.did_method import DIDMethods +from ....wallet.key_type import KeyTypes +from ....cache.base import BaseCache +from ....cache.in_memory import InMemoryCache + + +@pytest.mark.anoncreds +class TestCheqdDidManager(IsolatedAsyncioTestCase): + async def asyncSetUp(self): + did_methods = DIDMethods() + self.profile = await create_test_profile( + settings={"wallet.type": "askar-anoncreds"}, + ) + self.profile.context.injector.bind_instance(DIDMethods, did_methods) + self.profile.context.injector.bind_instance(KeyTypes, KeyTypes()) + self.logger = logging.getLogger(__name__) + self.profile.context.injector.bind_instance(BaseCache, InMemoryCache()) + + async def test_create_did(self): + response = await DidCheqdManager(self.profile).create({}) + did = response.get("did") + assert did.startswith("did:cheqd:testnet") + self.logger.info(f"DID: {did}") + assert response.get("verkey") diff --git a/acapy_agent/indy/models/cred_def.py b/acapy_agent/indy/models/cred_def.py index dbdaea2b2d..4c5547f96e 100644 --- a/acapy_agent/indy/models/cred_def.py +++ b/acapy_agent/indy/models/cred_def.py @@ -6,8 +6,8 @@ from ...messaging.valid import ( INDY_CRED_DEF_ID_EXAMPLE, INDY_CRED_DEF_ID_VALIDATE, - INDY_VERSION_EXAMPLE, - INDY_VERSION_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, NUM_STR_WHOLE_EXAMPLE, NUM_STR_WHOLE_VALIDATE, ) @@ -92,10 +92,10 @@ class CredentialDefinitionSchema(OpenAPISchema): """Marshmallow schema for indy cred def.""" ver = fields.Str( - validate=INDY_VERSION_VALIDATE, + validate=MAJOR_MINOR_VERSION_VALIDATE, metadata={ "description": "Node protocol version", - "example": INDY_VERSION_EXAMPLE, + "example": MAJOR_MINOR_VERSION_EXAMPLE, }, ) ident = fields.Str( diff --git a/acapy_agent/indy/models/pres_preview.py b/acapy_agent/indy/models/pres_preview.py index 496c1e93cd..d9e740193e 100644 --- a/acapy_agent/indy/models/pres_preview.py +++ b/acapy_agent/indy/models/pres_preview.py @@ -16,8 +16,8 @@ from ...messaging.valid import ( INDY_CRED_DEF_ID_EXAMPLE, INDY_CRED_DEF_ID_VALIDATE, - INDY_PREDICATE_EXAMPLE, - INDY_PREDICATE_VALIDATE, + PREDICATE_EXAMPLE, + PREDICATE_VALIDATE, ) from ...multitenant.base import BaseMultitenantManager from ...protocols.didcomm_prefix import DIDCommPrefix @@ -100,10 +100,10 @@ class Meta: ) predicate = fields.Str( required=True, - validate=INDY_PREDICATE_VALIDATE, + validate=PREDICATE_VALIDATE, metadata={ "description": "Predicate type ('<', '<=', '>=', or '>')", - "example": INDY_PREDICATE_EXAMPLE, + "example": PREDICATE_EXAMPLE, }, ) threshold = fields.Int( diff --git a/acapy_agent/indy/models/proof_request.py b/acapy_agent/indy/models/proof_request.py index 1c87ecf454..f27ac5a8f6 100644 --- a/acapy_agent/indy/models/proof_request.py +++ b/acapy_agent/indy/models/proof_request.py @@ -15,14 +15,14 @@ from ...messaging.models.openapi import OpenAPISchema from ...messaging.valid import ( INDY_CRED_DEF_ID_EXAMPLE, - INDY_PREDICATE_EXAMPLE, - INDY_PREDICATE_VALIDATE, - INDY_VERSION_EXAMPLE, - INDY_VERSION_VALIDATE, INT_EPOCH_EXAMPLE, INT_EPOCH_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, NUM_STR_NATURAL_EXAMPLE, NUM_STR_NATURAL_VALIDATE, + PREDICATE_EXAMPLE, + PREDICATE_VALIDATE, ) @@ -125,10 +125,10 @@ class IndyProofReqPredSpecSchema(OpenAPISchema): ) p_type = fields.Str( required=True, - validate=INDY_PREDICATE_VALIDATE, + validate=PREDICATE_VALIDATE, metadata={ "description": "Predicate type ('<', '<=', '>=', or '>')", - "example": INDY_PREDICATE_EXAMPLE, + "example": PREDICATE_EXAMPLE, }, ) p_value = fields.Int( @@ -251,10 +251,10 @@ class Meta: version = fields.Str( required=False, dump_default="1.0", - validate=INDY_VERSION_VALIDATE, + validate=MAJOR_MINOR_VERSION_VALIDATE, metadata={ "description": "Proof request version", - "example": INDY_VERSION_EXAMPLE, + "example": MAJOR_MINOR_VERSION_EXAMPLE, }, ) requested_attributes = fields.Dict( diff --git a/acapy_agent/indy/models/revocation.py b/acapy_agent/indy/models/revocation.py index a7a2795fbd..14c5617709 100644 --- a/acapy_agent/indy/models/revocation.py +++ b/acapy_agent/indy/models/revocation.py @@ -12,8 +12,8 @@ INDY_CRED_DEF_ID_VALIDATE, INDY_REV_REG_ID_EXAMPLE, INDY_REV_REG_ID_VALIDATE, - INDY_VERSION_EXAMPLE, - INDY_VERSION_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, NATURAL_NUM_EXAMPLE, NATURAL_NUM_VALIDATE, ) @@ -180,10 +180,10 @@ class Meta: unknown = EXCLUDE ver = fields.Str( - validate=INDY_VERSION_VALIDATE, + validate=MAJOR_MINOR_VERSION_VALIDATE, metadata={ "description": "Version of revocation registry definition", - "example": INDY_VERSION_EXAMPLE, + "example": MAJOR_MINOR_VERSION_EXAMPLE, }, ) id_ = fields.Str( @@ -294,10 +294,10 @@ class Meta: unknown = EXCLUDE ver = fields.Str( - validate=INDY_VERSION_VALIDATE, + validate=MAJOR_MINOR_VERSION_VALIDATE, metadata={ "description": "Version of revocation registry entry", - "example": INDY_VERSION_EXAMPLE, + "example": MAJOR_MINOR_VERSION_EXAMPLE, }, ) value = fields.Nested( diff --git a/acapy_agent/indy/models/schema.py b/acapy_agent/indy/models/schema.py index 5dff944356..df91802bc7 100644 --- a/acapy_agent/indy/models/schema.py +++ b/acapy_agent/indy/models/schema.py @@ -6,8 +6,8 @@ from ...messaging.valid import ( INDY_SCHEMA_ID_EXAMPLE, INDY_SCHEMA_ID_VALIDATE, - INDY_VERSION_EXAMPLE, - INDY_VERSION_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, NATURAL_NUM_EXAMPLE, NATURAL_NUM_VALIDATE, ) @@ -17,10 +17,10 @@ class SchemaSchema(OpenAPISchema): """Marshmallow schema for indy schema.""" ver = fields.Str( - validate=INDY_VERSION_VALIDATE, + validate=MAJOR_MINOR_VERSION_VALIDATE, metadata={ "description": "Node protocol version", - "example": INDY_VERSION_EXAMPLE, + "example": MAJOR_MINOR_VERSION_EXAMPLE, }, ) ident = fields.Str( @@ -38,8 +38,11 @@ class SchemaSchema(OpenAPISchema): } ) version = fields.Str( - validate=INDY_VERSION_VALIDATE, - metadata={"description": "Schema version", "example": INDY_VERSION_EXAMPLE}, + validate=MAJOR_MINOR_VERSION_VALIDATE, + metadata={ + "description": "Schema version", + "example": MAJOR_MINOR_VERSION_EXAMPLE, + }, ) attr_names = fields.List( fields.Str(metadata={"description": "Attribute name", "example": "score"}), diff --git a/acapy_agent/ledger/routes.py b/acapy_agent/ledger/routes.py index 4d63eb5af3..fee5e15261 100644 --- a/acapy_agent/ledger/routes.py +++ b/acapy_agent/ledger/routes.py @@ -25,10 +25,10 @@ ENDPOINT_VALIDATE, INDY_DID_EXAMPLE, INDY_DID_VALIDATE, - INDY_RAW_PUBLIC_KEY_EXAMPLE, - INDY_RAW_PUBLIC_KEY_VALIDATE, INT_EPOCH_EXAMPLE, INT_EPOCH_VALIDATE, + RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, UUID4_EXAMPLE, ) from ..multitenant.base import BaseMultitenantManager @@ -133,10 +133,10 @@ class RegisterLedgerNymQueryStringSchema(OpenAPISchema): ) verkey = fields.Str( required=True, - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Verification key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ) alias = fields.Str( @@ -230,10 +230,10 @@ class GetDIDVerkeyResponseSchema(OpenAPISchema): verkey = fields.Str( allow_none=True, - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Full verification key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ) diff --git a/acapy_agent/messaging/credential_definitions/util.py b/acapy_agent/messaging/credential_definitions/util.py index fb732ecd44..1cca544747 100644 --- a/acapy_agent/messaging/credential_definitions/util.py +++ b/acapy_agent/messaging/credential_definitions/util.py @@ -13,8 +13,8 @@ INDY_DID_VALIDATE, INDY_SCHEMA_ID_EXAMPLE, INDY_SCHEMA_ID_VALIDATE, - INDY_VERSION_EXAMPLE, - INDY_VERSION_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, ) CRED_DEF_SENT_RECORD_TYPE = "cred_def_sent" @@ -41,8 +41,11 @@ class CredDefQueryStringSchema(OpenAPISchema): ) schema_version = fields.Str( required=False, - validate=INDY_VERSION_VALIDATE, - metadata={"description": "Schema version", "example": INDY_VERSION_EXAMPLE}, + validate=MAJOR_MINOR_VERSION_VALIDATE, + metadata={ + "description": "Schema version", + "example": MAJOR_MINOR_VERSION_EXAMPLE, + }, ) issuer_did = fields.Str( required=False, diff --git a/acapy_agent/messaging/decorators/attach_decorator.py b/acapy_agent/messaging/decorators/attach_decorator.py index 98fc177996..7b17c6f670 100644 --- a/acapy_agent/messaging/decorators/attach_decorator.py +++ b/acapy_agent/messaging/decorators/attach_decorator.py @@ -29,8 +29,8 @@ BASE64_VALIDATE, BASE64URL_NO_PAD_EXAMPLE, BASE64URL_NO_PAD_VALIDATE, - INDY_ISO8601_DATETIME_EXAMPLE, - INDY_ISO8601_DATETIME_VALIDATE, + ISO8601_DATETIME_EXAMPLE, + ISO8601_DATETIME_VALIDATE, JWS_HEADER_KID_EXAMPLE, JWS_HEADER_KID_VALIDATE, SHA256_EXAMPLE, @@ -778,12 +778,12 @@ class Meta: ) lastmod_time = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": ( "Hint regarding last modification datetime, in ISO-8601 format" ), - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) description = fields.Str( diff --git a/acapy_agent/messaging/decorators/service_decorator.py b/acapy_agent/messaging/decorators/service_decorator.py index 0a4695a01c..e4f94b90e3 100644 --- a/acapy_agent/messaging/decorators/service_decorator.py +++ b/acapy_agent/messaging/decorators/service_decorator.py @@ -9,7 +9,10 @@ from marshmallow import EXCLUDE, fields from ..models.base import BaseModel, BaseModelSchema -from ..valid import INDY_RAW_PUBLIC_KEY_EXAMPLE, INDY_RAW_PUBLIC_KEY_VALIDATE +from ..valid import ( + RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, +) class ServiceDecorator(BaseModel): @@ -82,10 +85,10 @@ class Meta: recipient_keys = fields.List( fields.Str( - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Recipient public key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ), data_key="recipientKeys", @@ -102,10 +105,10 @@ class Meta: ) routing_keys = fields.List( fields.Str( - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Routing key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ), data_key="routingKeys", diff --git a/acapy_agent/messaging/decorators/signature_decorator.py b/acapy_agent/messaging/decorators/signature_decorator.py index 22ec1b38c1..ed7a217611 100644 --- a/acapy_agent/messaging/decorators/signature_decorator.py +++ b/acapy_agent/messaging/decorators/signature_decorator.py @@ -3,7 +3,7 @@ import json import struct import time -from typing import Optional +from typing import Optional, Tuple from marshmallow import EXCLUDE, fields @@ -15,8 +15,8 @@ from ..valid import ( BASE64URL_EXAMPLE, BASE64URL_VALIDATE, - INDY_RAW_PUBLIC_KEY_EXAMPLE, - INDY_RAW_PUBLIC_KEY_VALIDATE, + RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, ) @@ -86,7 +86,7 @@ async def create( signer=signer, ) - def decode(self) -> (object, int): + def decode(self) -> Tuple[object, int]: """Decode the signature to its timestamp and value. Returns: @@ -164,9 +164,9 @@ class Meta: ) signer = fields.Str( required=True, - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Signer verification key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ) diff --git a/acapy_agent/messaging/decorators/timing_decorator.py b/acapy_agent/messaging/decorators/timing_decorator.py index 7ad8fc8c86..cd8dd829d7 100644 --- a/acapy_agent/messaging/decorators/timing_decorator.py +++ b/acapy_agent/messaging/decorators/timing_decorator.py @@ -11,7 +11,7 @@ from ..models.base import BaseModel, BaseModelSchema from ..util import datetime_to_str -from ..valid import INDY_ISO8601_DATETIME_EXAMPLE, INDY_ISO8601_DATETIME_VALIDATE +from ..valid import ISO8601_DATETIME_EXAMPLE, ISO8601_DATETIME_VALIDATE class TimingDecorator(BaseModel): @@ -62,34 +62,34 @@ class Meta: in_time = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": "Time of message receipt", - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) out_time = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": "Time of message dispatch", - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) stale_time = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": "Time when message should be considered stale", - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) expires_time = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": "Time when message should be considered expired", - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) delay_milli = fields.Int( @@ -102,9 +102,9 @@ class Meta: ) wait_until_time = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": "Earliest time at which to perform processing", - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) diff --git a/acapy_agent/messaging/models/base_record.py b/acapy_agent/messaging/models/base_record.py index a89894dfc6..e613c6e6d9 100644 --- a/acapy_agent/messaging/models/base_record.py +++ b/acapy_agent/messaging/models/base_record.py @@ -20,7 +20,7 @@ ) from ...storage.record import StorageRecord from ..util import datetime_to_str, time_now -from ..valid import INDY_ISO8601_DATETIME_EXAMPLE, INDY_ISO8601_DATETIME_VALIDATE +from ..valid import ISO8601_DATETIME_EXAMPLE, ISO8601_DATETIME_VALIDATE from .base import BaseModel, BaseModelError, BaseModelSchema LOGGER = logging.getLogger(__name__) @@ -583,18 +583,18 @@ class Meta: ) created_at = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": "Time of record creation", - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) updated_at = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": "Time of last record update", - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) diff --git a/acapy_agent/messaging/schemas/routes.py b/acapy_agent/messaging/schemas/routes.py index d8128a5398..c31983d264 100644 --- a/acapy_agent/messaging/schemas/routes.py +++ b/acapy_agent/messaging/schemas/routes.py @@ -49,8 +49,8 @@ B58, INDY_SCHEMA_ID_EXAMPLE, INDY_SCHEMA_ID_VALIDATE, - INDY_VERSION_EXAMPLE, - INDY_VERSION_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, UUID4_EXAMPLE, ) from .util import ( @@ -70,8 +70,11 @@ class SchemaSendRequestSchema(OpenAPISchema): ) schema_version = fields.Str( required=True, - validate=INDY_VERSION_VALIDATE, - metadata={"description": "Schema version", "example": INDY_VERSION_EXAMPLE}, + validate=MAJOR_MINOR_VERSION_VALIDATE, + metadata={ + "description": "Schema version", + "example": MAJOR_MINOR_VERSION_EXAMPLE, + }, ) attributes = fields.List( fields.Str(metadata={"description": "attribute name", "example": "score"}), diff --git a/acapy_agent/messaging/schemas/util.py b/acapy_agent/messaging/schemas/util.py index 35f8ce5bae..9a3e0cf989 100644 --- a/acapy_agent/messaging/schemas/util.py +++ b/acapy_agent/messaging/schemas/util.py @@ -11,8 +11,8 @@ INDY_DID_VALIDATE, INDY_SCHEMA_ID_EXAMPLE, INDY_SCHEMA_ID_VALIDATE, - INDY_VERSION_EXAMPLE, - INDY_VERSION_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, ) @@ -37,8 +37,11 @@ class SchemaQueryStringSchema(OpenAPISchema): ) schema_version = fields.Str( required=False, - validate=INDY_VERSION_VALIDATE, - metadata={"description": "Schema version", "example": INDY_VERSION_EXAMPLE}, + validate=MAJOR_MINOR_VERSION_VALIDATE, + metadata={ + "description": "Schema version", + "example": MAJOR_MINOR_VERSION_EXAMPLE, + }, ) diff --git a/acapy_agent/messaging/tests/test_valid.py b/acapy_agent/messaging/tests/test_valid.py index 0f3b3d3363..b99737c4a8 100644 --- a/acapy_agent/messaging/tests/test_valid.py +++ b/acapy_agent/messaging/tests/test_valid.py @@ -19,20 +19,20 @@ INDY_CRED_REV_ID_VALIDATE, INDY_DID_VALIDATE, INDY_EXTRA_WQL_VALIDATE, - INDY_ISO8601_DATETIME_VALIDATE, - INDY_PREDICATE_VALIDATE, - INDY_RAW_PUBLIC_KEY_VALIDATE, INDY_REV_REG_ID_VALIDATE, INDY_REV_REG_SIZE_VALIDATE, INDY_SCHEMA_ID_VALIDATE, - INDY_VERSION_VALIDATE, INDY_WQL_VALIDATE, INT_EPOCH_VALIDATE, + ISO8601_DATETIME_VALIDATE, JWS_HEADER_KID_VALIDATE, JWT_VALIDATE, + MAJOR_MINOR_VERSION_VALIDATE, NATURAL_NUM_VALIDATE, NUM_STR_NATURAL_VALIDATE, NUM_STR_WHOLE_VALIDATE, + PREDICATE_VALIDATE, + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, SHA256_VALIDATE, UUID4_VALIDATE, WHOLE_NUM_VALIDATE, @@ -141,9 +141,11 @@ def test_indy_raw_public_key(self): ] for non_indy_raw_public_key in non_indy_raw_public_keys: with self.assertRaises(ValidationError): - INDY_RAW_PUBLIC_KEY_VALIDATE(non_indy_raw_public_key) + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE(non_indy_raw_public_key) - INDY_RAW_PUBLIC_KEY_VALIDATE("Q4zqM7aXqm7gDQkUVLng9hQ4zqM7aXqm7gDQkUVLng9h") + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE( + "Q4zqM7aXqm7gDQkUVLng9hQ4zqM7aXqm7gDQkUVLng9h" + ) def test_jws_header_kid(self): non_kids = [ @@ -283,12 +285,12 @@ def test_version(self): non_versions = ["-1", "", "3_5", "3.5a"] for non_version in non_versions: with self.assertRaises(ValidationError): - INDY_VERSION_VALIDATE(non_version) + MAJOR_MINOR_VERSION_VALIDATE(non_version) - INDY_VERSION_VALIDATE("1.0") - INDY_VERSION_VALIDATE(".05") - INDY_VERSION_VALIDATE("1.2.3") - INDY_VERSION_VALIDATE("..") # perverse but technically OK + MAJOR_MINOR_VERSION_VALIDATE("1.0") + MAJOR_MINOR_VERSION_VALIDATE(".05") + MAJOR_MINOR_VERSION_VALIDATE("1.2.3") + MAJOR_MINOR_VERSION_VALIDATE("..") # perverse but technically OK def test_schema_id(self): non_schema_ids = [ @@ -311,12 +313,12 @@ def test_predicate(self): non_predicates = [">>", "", " >= ", "<<<=", "==", "=", "!="] for non_predicate in non_predicates: with self.assertRaises(ValidationError): - INDY_PREDICATE_VALIDATE(non_predicate) + PREDICATE_VALIDATE(non_predicate) - INDY_PREDICATE_VALIDATE("<") - INDY_PREDICATE_VALIDATE("<=") - INDY_PREDICATE_VALIDATE(">=") - INDY_PREDICATE_VALIDATE(">") + PREDICATE_VALIDATE("<") + PREDICATE_VALIDATE("<=") + PREDICATE_VALIDATE(">=") + PREDICATE_VALIDATE(">") def test_indy_date(self): non_datetimes = [ @@ -329,17 +331,17 @@ def test_indy_date(self): ] for non_datetime in non_datetimes: with self.assertRaises(ValidationError): - INDY_ISO8601_DATETIME_VALIDATE(non_datetime) - - INDY_ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00Z") - INDY_ISO8601_DATETIME_VALIDATE("2020-01-01T00:00:00Z") - INDY_ISO8601_DATETIME_VALIDATE("2020-01-01T00:00:00") - INDY_ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00") - INDY_ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00+00:00") - INDY_ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00-00:00") - INDY_ISO8601_DATETIME_VALIDATE("2020-01-01 00:00-00:00") - INDY_ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00.1-00:00") - INDY_ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00.123456-00:00") + ISO8601_DATETIME_VALIDATE(non_datetime) + + ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00Z") + ISO8601_DATETIME_VALIDATE("2020-01-01T00:00:00Z") + ISO8601_DATETIME_VALIDATE("2020-01-01T00:00:00") + ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00") + ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00+00:00") + ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00-00:00") + ISO8601_DATETIME_VALIDATE("2020-01-01 00:00-00:00") + ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00.1-00:00") + ISO8601_DATETIME_VALIDATE("2020-01-01 00:00:00.123456-00:00") def test_indy_wql(self): non_wqls = [ diff --git a/acapy_agent/messaging/valid.py b/acapy_agent/messaging/valid.py index 27d1fef545..53b77a75b7 100644 --- a/acapy_agent/messaging/valid.py +++ b/acapy_agent/messaging/valid.py @@ -395,7 +395,7 @@ class CheqdCredDefId(Regexp): """Validate value against cheqd credential definition identifier specification.""" EXAMPLE = "did:cheqd:testnet:8a7e756c-d3b5-4947-af99-2dcd2e8cc5a2/resources/83f06db5-" - PATTERN = CheqdDID.RESOURCE_ID_PATTERN + PATTERN = CheqdDID.RESOURCE_ID_PATTERN.pattern def __init__(self): """Initialize the instance.""" @@ -410,7 +410,7 @@ class CheqdSchemaId(Regexp): """Validate value against cheqd schema identifier specification.""" EXAMPLE = "did:cheqd:testnet:8a7e756c-d3b5-4947-af99-2dcd2e8cc5a2/resources/e8cc28f2-" - PATTERN = CheqdDID.RESOURCE_ID_PATTERN + PATTERN = CheqdDID.RESOURCE_ID_PATTERN.pattern def __init__(self): """Initialize the instance.""" @@ -421,6 +421,21 @@ def __init__(self): ) +class AnoncredsDID(Regexp): + """Validate value against indy DID.""" + + EXAMPLE = "did:(method):WgWxqztrNooG92RXvxSTWv" + PATTERN = re.compile("^(did:[a-z]:.+$)?$" + "|" + CheqdDID.PATTERN.pattern) + + def __init__(self): + """Initialize the instance.""" + + super().__init__( + AnoncredsDID.PATTERN, + error="Value {input} is not an decentralized identifier (DID)", + ) + + class DIDValidation(Regexp): """Validate value against any valid DID spec.""" @@ -459,8 +474,8 @@ def __init__(self): ) -class IndyRawPublicKey(Regexp): - """Validate value against indy (Ed25519VerificationKey2018) raw public key.""" +class RawPublicEd25519VerificationKey2018(Regexp): + """Validate value against (Ed25519VerificationKey2018) raw public key.""" EXAMPLE = "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" PATTERN = rf"^[{B58}]{{43,44}}$" @@ -469,7 +484,7 @@ def __init__(self): """Initialize the instance.""" super().__init__( - IndyRawPublicKey.PATTERN, + RawPublicEd25519VerificationKey2018.PATTERN, error="Value {input} is not a raw Ed25519VerificationKey2018 key", ) @@ -482,7 +497,9 @@ class RoutingKey(Regexp): """ EXAMPLE = DIDKey.EXAMPLE - PATTERN = re.compile(DIDKey.PATTERN.pattern + "|" + IndyRawPublicKey.PATTERN) + PATTERN = re.compile( + DIDKey.PATTERN.pattern + "|" + RawPublicEd25519VerificationKey2018.PATTERN + ) def __init__(self): """Initialize the instance.""" @@ -517,8 +534,23 @@ def __init__(self): ) -class IndyVersion(Regexp): - """Validate value against indy version specification.""" +class AnoncredsCredDefId(Regexp): + """Validate value against anoncreds credential definition identifier specification.""" + + EXAMPLE = "did:(method):3:CL:20:tag" + PATTERN = re.compile(IndyCredDefId.PATTERN + "|" + CheqdCredDefId.PATTERN) + + def __init__(self): + """Initialize the instance.""" + + super().__init__( + AnoncredsCredDefId.PATTERN, + error="Value {input} is not an anoncreds credential definition identifier", + ) + + +class MajorMinorVersion(Regexp): + """Validate value against major minor version specification.""" EXAMPLE = "1.0" PATTERN = r"^[0-9.]+$" @@ -527,8 +559,8 @@ def __init__(self): """Initialize the instance.""" super().__init__( - IndyVersion.PATTERN, - error="Value {input} is not an indy version (use only digits and '.')", + MajorMinorVersion.PATTERN, + error="Value {input} is not a valid version major minor version (use only digits and '.')", # noqa: E501 ) @@ -547,6 +579,21 @@ def __init__(self): ) +class AnoncredsSchemaId(Regexp): + """Validate value against indy schema identifier specification.""" + + EXAMPLE = "did:(method):2:schema_name:1.0" + PATTERN = re.compile(IndySchemaId.PATTERN + "|" + CheqdSchemaId.PATTERN) + + def __init__(self): + """Initialize the instance.""" + + super().__init__( + AnoncredsSchemaId.PATTERN, + error="Value {input} is not an anoncreds schema identifier", + ) + + class IndyRevRegId(Regexp): """Validate value against indy revocation registry identifier specification.""" @@ -567,6 +614,21 @@ def __init__(self): ) +class AnoncredsRevRegId(Regexp): + """Validate value against anoncreds revocation registry identifier specification.""" + + EXAMPLE = "did:(method):4:did::3:CL:20:tag:CL_ACCUM:0" + PATTERN = r"^(.+$)" + + def __init__(self): + """Initialize the instance.""" + + super().__init__( + AnoncredsRevRegId.PATTERN, + error="Value {input} is not an anoncreds revocation registry identifier", + ) + + class IndyCredRevId(Regexp): """Validate value against indy credential revocation identifier specification.""" @@ -582,8 +644,8 @@ def __init__(self): ) -class IndyPredicate(OneOf): - """Validate value against indy predicate.""" +class Predicate(OneOf): + """Validate value against predicate.""" EXAMPLE = ">=" @@ -596,8 +658,8 @@ def __init__(self): ) -class IndyISO8601DateTime(Regexp): - """Validate value against ISO 8601 datetime format, indy profile.""" +class ISO8601DateTime(Regexp): + """Validate value against ISO 8601 datetime format.""" EXAMPLE = epoch_to_str(EXAMPLE_TIMESTAMP) PATTERN = ( @@ -609,7 +671,7 @@ def __init__(self): """Initialize the instance.""" super().__init__( - IndyISO8601DateTime.PATTERN, + ISO8601DateTime.PATTERN, error="Value {input} is not a date in valid format", ) @@ -1028,29 +1090,38 @@ def __init__( GENERIC_DID_VALIDATE = MaybeIndyDID() GENERIC_DID_EXAMPLE = MaybeIndyDID.EXAMPLE -INDY_RAW_PUBLIC_KEY_VALIDATE = IndyRawPublicKey() -INDY_RAW_PUBLIC_KEY_EXAMPLE = IndyRawPublicKey.EXAMPLE +RAW_ED25519_2018_PUBLIC_KEY_VALIDATE = RawPublicEd25519VerificationKey2018() +RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE = RawPublicEd25519VerificationKey2018.EXAMPLE INDY_SCHEMA_ID_VALIDATE = IndySchemaId() INDY_SCHEMA_ID_EXAMPLE = IndySchemaId.EXAMPLE +ANONCREDS_SCHEMA_ID_VALIDATE = AnoncredsSchemaId() +ANONCREDS_SCHEMA_ID_EXAMPLE = AnoncredsSchemaId.EXAMPLE + INDY_CRED_DEF_ID_VALIDATE = IndyCredDefId() INDY_CRED_DEF_ID_EXAMPLE = IndyCredDefId.EXAMPLE +ANONCREDS_CRED_DEF_ID_VALIDATE = AnoncredsCredDefId() +ANONCREDS_CRED_DEF_ID_EXAMPLE = AnoncredsCredDefId.EXAMPLE + INDY_REV_REG_ID_VALIDATE = IndyRevRegId() INDY_REV_REG_ID_EXAMPLE = IndyRevRegId.EXAMPLE +ANONCREDS_REV_REG_ID_VALIDATE = AnoncredsRevRegId() +ANONCREDS_REV_REG_ID_EXAMPLE = AnoncredsRevRegId.EXAMPLE + INDY_CRED_REV_ID_VALIDATE = IndyCredRevId() INDY_CRED_REV_ID_EXAMPLE = IndyCredRevId.EXAMPLE -INDY_VERSION_VALIDATE = IndyVersion() -INDY_VERSION_EXAMPLE = IndyVersion.EXAMPLE +MAJOR_MINOR_VERSION_VALIDATE = MajorMinorVersion() +MAJOR_MINOR_VERSION_EXAMPLE = MajorMinorVersion.EXAMPLE -INDY_PREDICATE_VALIDATE = IndyPredicate() -INDY_PREDICATE_EXAMPLE = IndyPredicate.EXAMPLE +PREDICATE_VALIDATE = Predicate() +PREDICATE_EXAMPLE = Predicate.EXAMPLE -INDY_ISO8601_DATETIME_VALIDATE = IndyISO8601DateTime() -INDY_ISO8601_DATETIME_EXAMPLE = IndyISO8601DateTime.EXAMPLE +ISO8601_DATETIME_VALIDATE = ISO8601DateTime() +ISO8601_DATETIME_EXAMPLE = ISO8601DateTime.EXAMPLE RFC3339_DATETIME_VALIDATE = RFC3339DateTime() RFC3339_DATETIME_EXAMPLE = RFC3339DateTime.EXAMPLE @@ -1105,3 +1176,6 @@ def __init__( INDY_OR_KEY_DID_VALIDATE = IndyOrKeyDID() INDY_OR_KEY_DID_EXAMPLE = IndyOrKeyDID.EXAMPLE + +ANONCREDS_DID_VALIDATE = AnoncredsDID() +ANONCREDS_DID_EXAMPLE = AnoncredsDID.EXAMPLE diff --git a/acapy_agent/protocols/basicmessage/v1_0/messages/basicmessage.py b/acapy_agent/protocols/basicmessage/v1_0/messages/basicmessage.py index 840be506b9..1a8940a904 100644 --- a/acapy_agent/protocols/basicmessage/v1_0/messages/basicmessage.py +++ b/acapy_agent/protocols/basicmessage/v1_0/messages/basicmessage.py @@ -8,8 +8,8 @@ from .....messaging.agent_message import AgentMessage, AgentMessageSchema from .....messaging.util import datetime_now, datetime_to_str from .....messaging.valid import ( - INDY_ISO8601_DATETIME_EXAMPLE, - INDY_ISO8601_DATETIME_VALIDATE, + ISO8601_DATETIME_EXAMPLE, + ISO8601_DATETIME_VALIDATE, ) from ..message_types import BASIC_MESSAGE, PROTOCOL_PACKAGE @@ -63,12 +63,12 @@ class Meta: sent_time = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": ( "Time message was sent, ISO8601 with space date/time separator" ), - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) content = fields.Str( diff --git a/acapy_agent/protocols/connections/v1_0/messages/connection_invitation.py b/acapy_agent/protocols/connections/v1_0/messages/connection_invitation.py index d983eb0ede..51e9f5f3c2 100644 --- a/acapy_agent/protocols/connections/v1_0/messages/connection_invitation.py +++ b/acapy_agent/protocols/connections/v1_0/messages/connection_invitation.py @@ -10,8 +10,8 @@ from .....messaging.valid import ( GENERIC_DID_EXAMPLE, GENERIC_DID_VALIDATE, - INDY_RAW_PUBLIC_KEY_EXAMPLE, - INDY_RAW_PUBLIC_KEY_VALIDATE, + RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, ) from .....wallet.util import b64_to_bytes, bytes_to_b64 from ..message_types import CONNECTION_INVITATION, PROTOCOL_PACKAGE @@ -131,10 +131,10 @@ class Meta: ) recipient_keys = fields.List( fields.Str( - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Recipient public key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ), data_key="recipientKeys", @@ -151,10 +151,10 @@ class Meta: ) routing_keys = fields.List( fields.Str( - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Routing key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ), data_key="routingKeys", diff --git a/acapy_agent/protocols/connections/v1_0/routes.py b/acapy_agent/protocols/connections/v1_0/routes.py index c6cbe47ecd..63344e5749 100644 --- a/acapy_agent/protocols/connections/v1_0/routes.py +++ b/acapy_agent/protocols/connections/v1_0/routes.py @@ -26,8 +26,8 @@ GENERIC_DID_VALIDATE, INDY_DID_EXAMPLE, INDY_DID_VALIDATE, - INDY_RAW_PUBLIC_KEY_EXAMPLE, - INDY_RAW_PUBLIC_KEY_VALIDATE, + RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, UUID4_EXAMPLE, UUID4_VALIDATE, ) @@ -91,10 +91,10 @@ class CreateInvitationRequestSchema(OpenAPISchema): recipient_keys = fields.List( fields.Str( - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Recipient public key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ), required=False, @@ -109,10 +109,10 @@ class CreateInvitationRequestSchema(OpenAPISchema): ) routing_keys = fields.List( fields.Str( - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Routing key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ), required=False, @@ -210,10 +210,10 @@ class ConnectionStaticResultSchema(OpenAPISchema): ) my_verkey = fields.Str( required=True, - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "My verification key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ) my_endpoint = fields.Str( @@ -228,10 +228,10 @@ class ConnectionStaticResultSchema(OpenAPISchema): ) their_verkey = fields.Str( required=True, - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Remote verification key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ) record = fields.Nested(ConnRecordSchema(), required=True) @@ -245,10 +245,10 @@ class ConnectionsListQueryStringSchema(PaginatedQuerySchema): ) invitation_key = fields.Str( required=False, - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "invitation key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ) my_did = fields.Str( diff --git a/acapy_agent/protocols/issue_credential/v1_0/messages/credential_proposal.py b/acapy_agent/protocols/issue_credential/v1_0/messages/credential_proposal.py index 05416c54b1..d53ce0e549 100644 --- a/acapy_agent/protocols/issue_credential/v1_0/messages/credential_proposal.py +++ b/acapy_agent/protocols/issue_credential/v1_0/messages/credential_proposal.py @@ -12,8 +12,8 @@ INDY_DID_VALIDATE, INDY_SCHEMA_ID_EXAMPLE, INDY_SCHEMA_ID_VALIDATE, - INDY_VERSION_EXAMPLE, - INDY_VERSION_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, ) from ..message_types import CREDENTIAL_PROPOSAL, PROTOCOL_PACKAGE from .inner.credential_preview import CredentialPreview, CredentialPreviewSchema @@ -104,8 +104,8 @@ class Meta: schema_version = fields.Str( required=False, allow_none=False, - validate=INDY_VERSION_VALIDATE, - metadata={"example": INDY_VERSION_EXAMPLE}, + validate=MAJOR_MINOR_VERSION_VALIDATE, + metadata={"example": MAJOR_MINOR_VERSION_EXAMPLE}, ) cred_def_id = fields.Str( required=False, diff --git a/acapy_agent/protocols/issue_credential/v1_0/routes.py b/acapy_agent/protocols/issue_credential/v1_0/routes.py index 725ad02992..1b5581a51b 100644 --- a/acapy_agent/protocols/issue_credential/v1_0/routes.py +++ b/acapy_agent/protocols/issue_credential/v1_0/routes.py @@ -31,8 +31,8 @@ INDY_DID_VALIDATE, INDY_SCHEMA_ID_EXAMPLE, INDY_SCHEMA_ID_VALIDATE, - INDY_VERSION_EXAMPLE, - INDY_VERSION_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, UUID4_EXAMPLE, UUID4_VALIDATE, ) @@ -139,8 +139,11 @@ class V10CredentialCreateSchema(AdminAPIMessageTracingSchema): ) schema_version = fields.Str( required=False, - validate=INDY_VERSION_VALIDATE, - metadata={"description": "Schema version", "example": INDY_VERSION_EXAMPLE}, + validate=MAJOR_MINOR_VERSION_VALIDATE, + metadata={ + "description": "Schema version", + "example": MAJOR_MINOR_VERSION_EXAMPLE, + }, ) issuer_did = fields.Str( required=False, @@ -198,8 +201,11 @@ class V10CredentialProposalRequestSchemaBase(AdminAPIMessageTracingSchema): ) schema_version = fields.Str( required=False, - validate=INDY_VERSION_VALIDATE, - metadata={"description": "Schema version", "example": INDY_VERSION_EXAMPLE}, + validate=MAJOR_MINOR_VERSION_VALIDATE, + metadata={ + "description": "Schema version", + "example": MAJOR_MINOR_VERSION_EXAMPLE, + }, ) issuer_did = fields.Str( required=False, diff --git a/acapy_agent/protocols/issue_credential/v2_0/formats/anoncreds/handler.py b/acapy_agent/protocols/issue_credential/v2_0/formats/anoncreds/handler.py index e72614437a..a25e4bb62c 100644 --- a/acapy_agent/protocols/issue_credential/v2_0/formats/anoncreds/handler.py +++ b/acapy_agent/protocols/issue_credential/v2_0/formats/anoncreds/handler.py @@ -1,30 +1,28 @@ -"""V2.0 issue-credential indy credential format handler.""" +"""V2.0 issue-credential anoncreds credential format handler.""" import json import logging from typing import Mapping, Optional, Tuple +from anoncreds import CredentialDefinition, Schema from marshmallow import RAISE +from ......anoncreds.base import AnonCredsResolutionError from ......anoncreds.holder import AnonCredsHolder, AnonCredsHolderError -from ......anoncreds.issuer import AnonCredsIssuer +from ......anoncreds.issuer import CATEGORY_CRED_DEF, CATEGORY_SCHEMA, AnonCredsIssuer +from ......anoncreds.models.credential import AnoncredsCredentialSchema +from ......anoncreds.models.credential_offer import AnoncredsCredentialOfferSchema +from ......anoncreds.models.credential_proposal import ( + AnoncredsCredentialDefinitionProposal, +) +from ......anoncreds.models.credential_request import AnoncredsCredRequestSchema from ......anoncreds.registry import AnonCredsRegistry from ......anoncreds.revocation import AnonCredsRevocation from ......cache.base import BaseCache -from ......indy.models.cred import IndyCredentialSchema -from ......indy.models.cred_abstract import IndyCredAbstractSchema -from ......indy.models.cred_request import IndyCredRequestSchema -from ......ledger.base import BaseLedger -from ......ledger.multiple_ledger.ledger_requests_executor import ( - GET_CRED_DEF, - IndyLedgerRequestsExecutor, -) from ......messaging.credential_definitions.util import ( CRED_DEF_SENT_RECORD_TYPE, - CredDefQueryStringSchema, ) from ......messaging.decorators.attach_decorator import AttachDecorator -from ......multitenant.base import BaseMultitenantManager from ......revocation_anoncreds.models.issuer_cred_rev_record import IssuerCredRevRecord from ......storage.base import BaseStorage from ...message_types import ( @@ -40,16 +38,16 @@ from ...messages.cred_proposal import V20CredProposal from ...messages.cred_request import V20CredRequest from ...models.cred_ex_record import V20CredExRecord -from ...models.detail.indy import V20CredExRecordIndy +from ...models.detail.anoncreds import V20CredExRecordAnoncreds from ..handler import CredFormatAttachment, V20CredFormatError, V20CredFormatHandler LOGGER = logging.getLogger(__name__) class AnonCredsCredFormatHandler(V20CredFormatHandler): - """Indy credential format handler.""" + """Anoncreds credential format handler.""" - format = V20CredFormat.Format.INDY + format = V20CredFormat.Format.ANONCREDS @classmethod def validate_fields(cls, message_type: str, attachment_data: Mapping): @@ -71,10 +69,10 @@ def validate_fields(cls, message_type: str, attachment_data: Mapping): """ mapping = { - CRED_20_PROPOSAL: CredDefQueryStringSchema, - CRED_20_OFFER: IndyCredAbstractSchema, - CRED_20_REQUEST: IndyCredRequestSchema, - CRED_20_ISSUE: IndyCredentialSchema, + CRED_20_PROPOSAL: AnoncredsCredentialDefinitionProposal, + CRED_20_OFFER: AnoncredsCredentialOfferSchema, + CRED_20_REQUEST: AnoncredsCredRequestSchema, + CRED_20_ISSUE: AnoncredsCredentialSchema, } # Get schema class @@ -83,7 +81,7 @@ def validate_fields(cls, message_type: str, attachment_data: Mapping): # Validate, throw if not valid Schema(unknown=RAISE).load(attachment_data) - async def get_detail_record(self, cred_ex_id: str) -> V20CredExRecordIndy: + async def get_detail_record(self, cred_ex_id: str) -> V20CredExRecordAnoncreds: """Retrieve credential exchange detail record by cred_ex_id.""" async with self.profile.session() as session: @@ -167,7 +165,7 @@ async def _match_sent_cred_def_id(self, tag_query: Mapping[str, str]) -> str: async def create_proposal( self, cred_ex_record: V20CredExRecord, proposal_data: Mapping[str, str] ) -> Tuple[V20CredFormat, AttachDecorator]: - """Create indy credential proposal.""" + """Create anoncreds credential proposal.""" if proposal_data is None: proposal_data = {} @@ -176,7 +174,7 @@ async def create_proposal( async def receive_proposal( self, cred_ex_record: V20CredExRecord, cred_proposal_message: V20CredProposal ) -> None: - """Receive indy credential proposal. + """Receive anoncreds credential proposal. No custom handling is required for this step. """ @@ -184,35 +182,37 @@ async def receive_proposal( async def create_offer( self, cred_proposal_message: V20CredProposal ) -> CredFormatAttachment: - """Create indy credential offer.""" + """Create anoncreds credential offer.""" issuer = AnonCredsIssuer(self.profile) - ledger = self.profile.inject(BaseLedger) cache = self.profile.inject_or(BaseCache) + anoncreds_attachment = cred_proposal_message.attachment( + AnonCredsCredFormatHandler.format + ) + + if not anoncreds_attachment: + anoncreds_attachment = cred_proposal_message.attachment( + V20CredFormat.Format.INDY.api + ) + cred_def_id = await issuer.match_created_credential_definitions( - **cred_proposal_message.attachment(AnonCredsCredFormatHandler.format) + **anoncreds_attachment ) async def _create(): offer_json = await issuer.create_credential_offer(cred_def_id) return json.loads(offer_json) - multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) - if multitenant_mgr: - ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) - else: - ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) - ledger = ( - await ledger_exec_inst.get_ledger_for_identifier( - cred_def_id, - txn_record_type=GET_CRED_DEF, + async with self.profile.session() as session: + cred_def_entry = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id) + cred_def_dict = CredentialDefinition.load(cred_def_entry.value).to_dict() + schema_entry = await session.handle.fetch( + CATEGORY_SCHEMA, cred_def_dict["schemaId"] ) - )[1] - async with ledger: - schema_id = await ledger.credential_definition_id2schema_id(cred_def_id) - schema = await ledger.get_schema(schema_id) - schema_attrs = set(schema["attrNames"]) + schema_dict = Schema.load(schema_entry.value).to_dict() + + schema_attrs = set(schema_dict["attrNames"]) preview_attrs = set(cred_proposal_message.credential_preview.attr_dict()) if preview_attrs != schema_attrs: raise V20CredFormatError( @@ -238,23 +238,26 @@ async def _create(): async def receive_offer( self, cred_ex_record: V20CredExRecord, cred_offer_message: V20CredOffer ) -> None: - """Receive indy credential offer.""" + """Receive anoncreds credential offer.""" async def create_request( self, cred_ex_record: V20CredExRecord, request_data: Optional[Mapping] = None ) -> CredFormatAttachment: - """Create indy credential request.""" + """Create anoncreds credential request.""" if cred_ex_record.state != V20CredExRecord.STATE_OFFER_RECEIVED: raise V20CredFormatError( - "Indy issue credential format cannot start from credential request" + "Anoncreds issue credential format cannot start from credential request" ) await self._check_uniqueness(cred_ex_record.cred_ex_id) - holder_did = request_data.get("holder_did") if request_data else None + + # For backwards compatibility, remove indy backup when indy format is retired + from ..indy.handler import IndyCredFormatHandler + cred_offer = cred_ex_record.cred_offer.attachment( AnonCredsCredFormatHandler.format - ) + ) or cred_ex_record.cred_offer.attachment(IndyCredFormatHandler.format) if "nonce" not in cred_offer: raise V20CredFormatError("Missing nonce in credential offer") @@ -265,19 +268,24 @@ async def create_request( async def _create(): anoncreds_registry = self.profile.inject(AnonCredsRegistry) - cred_def_result = await anoncreds_registry.get_credential_definition( - self.profile, cred_def_id - ) - - holder = AnonCredsHolder(self.profile) - request_json, metadata_json = await holder.create_credential_request( - cred_offer, cred_def_result.credential_definition, holder_did - ) + try: + cred_def_result = await anoncreds_registry.get_credential_definition( + self.profile, cred_def_id + ) + holder = AnonCredsHolder(self.profile) + request_json, metadata_json = await holder.create_credential_request( + cred_offer, cred_def_result.credential_definition, holder_did + ) - return { - "request": json.loads(request_json), - "metadata": json.loads(metadata_json), - } + return { + "request": json.loads(request_json), + "metadata": json.loads(metadata_json), + } + # This is for compatability with a holder that isn't anoncreds capable + except AnonCredsResolutionError: + return await IndyCredFormatHandler.create_cred_request_result( + self, cred_offer, holder_did, cred_def_id + ) cache_key = f"credential_request::{cred_def_id}::{holder_did}::{nonce}" cred_req_result = None @@ -292,7 +300,7 @@ async def _create(): if not cred_req_result: cred_req_result = await _create() - detail_record = V20CredExRecordIndy( + detail_record = V20CredExRecordAnoncreds( cred_ex_id=cred_ex_record.cred_ex_id, cred_request_metadata=cred_req_result["metadata"], ) @@ -305,18 +313,25 @@ async def _create(): async def receive_request( self, cred_ex_record: V20CredExRecord, cred_request_message: V20CredRequest ) -> None: - """Receive indy credential request.""" + """Receive anoncreds credential request.""" if not cred_ex_record.cred_offer: raise V20CredFormatError( - "Indy issue credential format cannot start from credential request" + "Anoncreds issue credential format cannot start from credential request" ) async def issue_credential( self, cred_ex_record: V20CredExRecord, retries: int = 5 ) -> CredFormatAttachment: - """Issue indy credential.""" + """Issue anoncreds credential.""" await self._check_uniqueness(cred_ex_record.cred_ex_id) + # For backwards compatibility, remove indy backup when indy format is retired + from ..indy.handler import IndyCredFormatHandler + + if cred_ex_record.cred_offer.attachment(IndyCredFormatHandler.format): + indy_handler = IndyCredFormatHandler(self.profile) + return await indy_handler.issue_credential(cred_ex_record, retries) + cred_offer = cred_ex_record.cred_offer.attachment( AnonCredsCredFormatHandler.format ) @@ -342,7 +357,7 @@ async def issue_credential( result = self.get_format_data(CRED_20_ISSUE, json.loads(cred_json)) async with self._profile.transaction() as txn: - detail_record = V20CredExRecordIndy( + detail_record = V20CredExRecordAnoncreds( cred_ex_id=cred_ex_record.cred_ex_id, rev_reg_id=rev_reg_def_id, cred_rev_id=cred_rev_id, @@ -371,7 +386,7 @@ async def issue_credential( async def receive_credential( self, cred_ex_record: V20CredExRecord, cred_issue_message: V20CredIssue ) -> None: - """Receive indy credential. + """Receive anoncreds credential. Validation is done in the store credential step. """ @@ -379,21 +394,33 @@ async def receive_credential( async def store_credential( self, cred_ex_record: V20CredExRecord, cred_id: Optional[str] = None ) -> None: - """Store indy credential.""" - cred = cred_ex_record.cred_issue.attachment(AnonCredsCredFormatHandler.format) + """Store anoncreds credential.""" + + # For backwards compatibility, remove indy backup when indy format is retired + from ..indy.handler import IndyCredFormatHandler + + cred = cred_ex_record.cred_issue.attachment( + AnonCredsCredFormatHandler.format + ) or cred_ex_record.cred_issue.attachment(IndyCredFormatHandler.format) rev_reg_def = None anoncreds_registry = self.profile.inject(AnonCredsRegistry) - cred_def_result = await anoncreds_registry.get_credential_definition( - self.profile, cred["cred_def_id"] - ) - if cred.get("rev_reg_id"): - rev_reg_def_result = ( - await anoncreds_registry.get_revocation_registry_definition( - self.profile, cred["rev_reg_id"] + try: + cred_def_result = await anoncreds_registry.get_credential_definition( + self.profile, cred["cred_def_id"] + ) + if cred.get("rev_reg_id"): + rev_reg_def_result = ( + await anoncreds_registry.get_revocation_registry_definition( + self.profile, cred["rev_reg_id"] + ) ) + rev_reg_def = rev_reg_def_result.revocation_registry + + except AnonCredsResolutionError: + return await IndyCredFormatHandler.store_credential( + self, cred_ex_record, cred_id ) - rev_reg_def = rev_reg_def_result.revocation_registry holder = AnonCredsHolder(self.profile) cred_offer_message = cred_ex_record.cred_offer diff --git a/acapy_agent/protocols/issue_credential/v2_0/formats/anoncreds/tests/test_handler.py b/acapy_agent/protocols/issue_credential/v2_0/formats/anoncreds/tests/test_handler.py index 46606417fb..fed47beaf3 100644 --- a/acapy_agent/protocols/issue_credential/v2_0/formats/anoncreds/tests/test_handler.py +++ b/acapy_agent/protocols/issue_credential/v2_0/formats/anoncreds/tests/test_handler.py @@ -36,10 +36,10 @@ from ....messages.cred_request import V20CredRequest from ....messages.inner.cred_preview import V20CredAttrSpec, V20CredPreview from ....models.cred_ex_record import V20CredExRecord -from ....models.detail.indy import V20CredExRecordIndy +from ....models.detail.anoncreds import V20CredExRecordAnoncreds from ...handler import V20CredFormatError from .. import handler as test_module -from ..handler import LOGGER as INDY_LOGGER +from ..handler import LOGGER as ANONCREDS_LOGGER from ..handler import AnonCredsCredFormatHandler TEST_DID = "LjgpST2rjsoxYegQDRm7EL" @@ -108,7 +108,7 @@ "tailsLocation": TAILS_LOCAL, }, } -INDY_OFFER = { +ANONCREDS_OFFER = { "schema_id": SCHEMA_ID, "cred_def_id": CRED_DEF_ID, "key_correctness_proof": { @@ -131,7 +131,7 @@ }, "nonce": "1234567890", } -INDY_CRED_REQ = { +ANONCREDS_CRED_REQ = { "prover_did": TEST_DID, "cred_def_id": CRED_DEF_ID, "blinded_ms": { @@ -148,7 +148,7 @@ }, "nonce": "9876543210", } -INDY_CRED = { +ANONCREDS_CRED = { "schema_id": SCHEMA_ID, "cred_def_id": CRED_DEF_ID, "rev_reg_id": REV_REG_ID, @@ -232,9 +232,9 @@ async def asyncSetUp(self): async def test_validate_fields(self): # Test correct data self.handler.validate_fields(CRED_20_PROPOSAL, {"cred_def_id": CRED_DEF_ID}) - self.handler.validate_fields(CRED_20_OFFER, INDY_OFFER) - self.handler.validate_fields(CRED_20_REQUEST, INDY_CRED_REQ) - self.handler.validate_fields(CRED_20_ISSUE, INDY_CRED) + self.handler.validate_fields(CRED_20_OFFER, ANONCREDS_OFFER) + self.handler.validate_fields(CRED_20_REQUEST, ANONCREDS_CRED_REQ) + self.handler.validate_fields(CRED_20_ISSUE, ANONCREDS_CRED) # test incorrect proposal with self.assertRaises(ValidationError): @@ -244,31 +244,31 @@ async def test_validate_fields(self): # test incorrect offer with self.assertRaises(ValidationError): - offer = INDY_OFFER.copy() + offer = ANONCREDS_OFFER.copy() offer.pop("nonce") self.handler.validate_fields(CRED_20_OFFER, offer) # test incorrect request with self.assertRaises(ValidationError): - req = INDY_CRED_REQ.copy() + req = ANONCREDS_CRED_REQ.copy() req.pop("nonce") self.handler.validate_fields(CRED_20_REQUEST, req) # test incorrect cred with self.assertRaises(ValidationError): - cred = INDY_CRED.copy() + cred = ANONCREDS_CRED.copy() cred.pop("schema_id") self.handler.validate_fields(CRED_20_ISSUE, cred) async def test_get_indy_detail_record(self): cred_ex_id = "dummy" details_indy = [ - V20CredExRecordIndy( + V20CredExRecordAnoncreds( cred_ex_id=cred_ex_id, rev_reg_id="rr-id", cred_rev_id="0", ), - V20CredExRecordIndy( + V20CredExRecordAnoncreds( cred_ex_id=cred_ex_id, rev_reg_id="rr-id", cred_rev_id="1", @@ -278,7 +278,9 @@ async def test_get_indy_detail_record(self): await details_indy[0].save(session) await details_indy[1].save(session) # exercise logger warning on get() - with mock.patch.object(INDY_LOGGER, "warning", mock.MagicMock()) as mock_warning: + with mock.patch.object( + ANONCREDS_LOGGER, "warning", mock.MagicMock() + ) as mock_warning: assert await self.handler.get_detail_record(cred_ex_id) in details_indy mock_warning.assert_called_once() @@ -354,7 +356,7 @@ async def test_create_offer(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], @@ -379,7 +381,7 @@ async def test_create_offer(self): await self.session.storage.add_record(cred_def_record) self.issuer.create_credential_offer = mock.CoroutineMock( - return_value=json.dumps(INDY_OFFER) + return_value=json.dumps(ANONCREDS_OFFER) ) (cred_format, attachment) = await self.handler.create_offer(cred_proposal) @@ -390,7 +392,7 @@ async def test_create_offer(self): assert cred_format.attach_id == self.handler.format.api == attachment.ident # assert content of attachment is proposal data - assert attachment.content == INDY_OFFER + assert attachment.content == ANONCREDS_OFFER # assert data is encoded as base64 assert attachment.data.base64 @@ -417,7 +419,7 @@ async def test_create_offer_no_cache(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], @@ -446,7 +448,7 @@ async def test_create_offer_no_cache(self): await self.session.storage.add_record(cred_def_record) self.issuer.create_credential_offer = mock.CoroutineMock( - return_value=json.dumps(INDY_OFFER) + return_value=json.dumps(ANONCREDS_OFFER) ) (cred_format, attachment) = await self.handler.create_offer(cred_proposal) @@ -457,7 +459,7 @@ async def test_create_offer_no_cache(self): assert cred_format.attach_id == self.handler.format.api == attachment.ident # assert content of attachment is proposal data - assert attachment.content == INDY_OFFER + assert attachment.content == ANONCREDS_OFFER # assert data is encoded as base64 assert attachment.data.base64 @@ -480,7 +482,7 @@ async def test_create_offer_attr_mismatch(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], @@ -509,7 +511,7 @@ async def test_create_offer_attr_mismatch(self): await self.session.storage.add_record(cred_def_record) self.issuer.create_credential_offer = mock.CoroutineMock( - return_value=json.dumps(INDY_OFFER) + return_value=json.dumps(ANONCREDS_OFFER) ) with mock.patch.object( IndyLedgerRequestsExecutor, @@ -526,7 +528,7 @@ async def test_create_offer_no_matching_sent_cred_def(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], @@ -534,7 +536,7 @@ async def test_create_offer_no_matching_sent_cred_def(self): ) self.issuer.create_credential_offer = mock.CoroutineMock( - return_value=json.dumps(INDY_OFFER) + return_value=json.dumps(ANONCREDS_OFFER) ) with self.assertRaises(V20CredFormatError) as context: @@ -557,11 +559,11 @@ async def test_create_request(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + offers_attach=[AttachDecorator.data_base64(ANONCREDS_OFFER, ident="0")], ) cred_ex_record = V20CredExRecord( cred_ex_id="dummy-id", @@ -574,7 +576,7 @@ async def test_create_request(self): cred_req_meta = {} self.holder.create_credential_request = mock.CoroutineMock( - return_value=(json.dumps(INDY_CRED_REQ), json.dumps(cred_req_meta)) + return_value=(json.dumps(ANONCREDS_CRED_REQ), json.dumps(cred_req_meta)) ) (cred_format, attachment) = await self.handler.create_request( @@ -582,14 +584,14 @@ async def test_create_request(self): ) self.holder.create_credential_request.assert_called_once_with( - INDY_OFFER, cred_def, holder_did + ANONCREDS_OFFER, cred_def, holder_did ) # assert identifier match assert cred_format.attach_id == self.handler.format.api == attachment.ident # assert content of attachment is proposal data - assert attachment.content == INDY_CRED_REQ + assert attachment.content == ANONCREDS_CRED_REQ # assert data is encoded as base64 assert attachment.data.base64 @@ -617,16 +619,18 @@ async def test_create_request_bad_state(self): with self.assertRaises(V20CredFormatError) as context: await self.handler.create_request(cred_ex_record) - assert "Indy issue credential format cannot start from credential request" in str( - context.exception + assert ( + "Anoncreds issue credential format cannot start from credential request" + in str(context.exception) ) cred_ex_record.state = None with self.assertRaises(V20CredFormatError) as context: await self.handler.create_request(cred_ex_record) - assert "Indy issue credential format cannot start from credential request" in str( - context.exception + assert ( + "Anoncreds issue credential format cannot start from credential request" + in str(context.exception) ) async def test_create_request_not_unique_x(self): @@ -658,8 +662,9 @@ async def test_receive_request_no_offer(self): with self.assertRaises(V20CredFormatError) as context: await self.handler.receive_request(cred_ex_record, cred_request_message) - assert "Indy issue credential format cannot start from credential request" in str( - context.exception + assert ( + "Anoncreds issue credential format cannot start from credential request" + in str(context.exception) ) @pytest.mark.skip(reason="Anoncreds-break") @@ -680,22 +685,22 @@ async def test_issue_credential_revocable(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + offers_attach=[AttachDecorator.data_base64(ANONCREDS_OFFER, ident="0")], ) cred_request = V20CredRequest( formats=[ V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + requests_attach=[AttachDecorator.data_base64(ANONCREDS_CRED_REQ, ident="0")], ) cred_ex_record = V20CredExRecord( @@ -709,7 +714,7 @@ async def test_issue_credential_revocable(self): cred_rev_id = "1000" self.issuer.create_credential = mock.CoroutineMock( - return_value=(json.dumps(INDY_CRED), cred_rev_id) + return_value=(json.dumps(ANONCREDS_CRED), cred_rev_id) ) with mock.patch.object(test_module, "IndyRevocation", autospec=True) as revoc: @@ -732,8 +737,8 @@ async def test_issue_credential_revocable(self): self.issuer.create_credential.assert_called_once_with( SCHEMA, - INDY_OFFER, - INDY_CRED_REQ, + ANONCREDS_OFFER, + ANONCREDS_CRED_REQ, attr_values, REV_REG_ID, "dummy-path", @@ -743,7 +748,7 @@ async def test_issue_credential_revocable(self): assert cred_format.attach_id == self.handler.format.api == attachment.ident # assert content of attachment is proposal data - assert attachment.content == INDY_CRED + assert attachment.content == ANONCREDS_CRED # assert data is encoded as base64 assert attachment.data.base64 @@ -768,22 +773,22 @@ async def test_issue_credential_non_revocable(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + offers_attach=[AttachDecorator.data_base64(ANONCREDS_OFFER, ident="0")], ) cred_request = V20CredRequest( formats=[ V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + requests_attach=[AttachDecorator.data_base64(ANONCREDS_CRED_REQ, ident="0")], ) cred_ex_record = V20CredExRecord( @@ -796,7 +801,7 @@ async def test_issue_credential_non_revocable(self): ) self.issuer.create_credential = mock.CoroutineMock( - return_value=(json.dumps(INDY_CRED), None) + return_value=(json.dumps(ANONCREDS_CRED), None) ) self.ledger.get_credential_definition = mock.CoroutineMock( return_value=CRED_DEF_NR @@ -816,8 +821,8 @@ async def test_issue_credential_non_revocable(self): self.issuer.create_credential.assert_called_once_with( SCHEMA, - INDY_OFFER, - INDY_CRED_REQ, + ANONCREDS_OFFER, + ANONCREDS_CRED_REQ, attr_values, None, None, @@ -827,7 +832,7 @@ async def test_issue_credential_non_revocable(self): assert cred_format.attach_id == self.handler.format.api == attachment.ident # assert content of attachment is proposal data - assert attachment.content == INDY_CRED + assert attachment.content == ANONCREDS_CRED # assert data is encoded as base64 assert attachment.data.base64 @@ -867,22 +872,22 @@ async def test_issue_credential_no_active_rr_no_retries(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + offers_attach=[AttachDecorator.data_base64(ANONCREDS_OFFER, ident="0")], ) cred_request = V20CredRequest( formats=[ V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + requests_attach=[AttachDecorator.data_base64(ANONCREDS_CRED_REQ, ident="0")], ) cred_ex_record = V20CredExRecord( @@ -895,7 +900,7 @@ async def test_issue_credential_no_active_rr_no_retries(self): ) self.issuer.create_credential = mock.CoroutineMock( - return_value=(json.dumps(INDY_CRED), cred_rev_id) + return_value=(json.dumps(ANONCREDS_CRED), cred_rev_id) ) with mock.patch.object(test_module, "IndyRevocation", autospec=True) as revoc: @@ -926,22 +931,22 @@ async def test_issue_credential_no_active_rr_retry(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + offers_attach=[AttachDecorator.data_base64(ANONCREDS_OFFER, ident="0")], ) cred_request = V20CredRequest( formats=[ V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + requests_attach=[AttachDecorator.data_base64(ANONCREDS_CRED_REQ, ident="0")], ) cred_ex_record = V20CredExRecord( @@ -954,7 +959,7 @@ async def test_issue_credential_no_active_rr_retry(self): ) self.issuer.create_credential = mock.CoroutineMock( - return_value=(json.dumps(INDY_CRED), cred_rev_id) + return_value=(json.dumps(ANONCREDS_CRED), cred_rev_id) ) with mock.patch.object(test_module, "IndyRevocation", autospec=True) as revoc: @@ -996,22 +1001,22 @@ async def test_issue_credential_rr_full(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + offers_attach=[AttachDecorator.data_base64(ANONCREDS_OFFER, ident="0")], ) cred_request = V20CredRequest( formats=[ V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + requests_attach=[AttachDecorator.data_base64(ANONCREDS_CRED_REQ, ident="0")], ) cred_ex_record = V20CredExRecord( @@ -1075,11 +1080,11 @@ async def test_store_credential(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + offers_attach=[AttachDecorator.data_base64(ANONCREDS_OFFER, ident="0")], ) cred_offer.assign_thread_id(thread_id) cred_request = V20CredRequest( @@ -1087,22 +1092,22 @@ async def test_store_credential(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + requests_attach=[AttachDecorator.data_base64(ANONCREDS_CRED_REQ, ident="0")], ) cred_issue = V20CredIssue( formats=[ V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_ISSUE][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - credentials_attach=[AttachDecorator.data_base64(INDY_CRED, ident="0")], + credentials_attach=[AttachDecorator.data_base64(ANONCREDS_CRED, ident="0")], ) stored_cx_rec = V20CredExRecord( @@ -1167,7 +1172,7 @@ async def test_store_credential(self): self.holder.store_credential.assert_called_once_with( CRED_DEF, - INDY_CRED, + ANONCREDS_CRED, cred_req_meta, {"pic": "image/jpeg"}, credential_id=cred_id, @@ -1198,11 +1203,11 @@ async def test_store_credential_holder_store_indy_error(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + offers_attach=[AttachDecorator.data_base64(ANONCREDS_OFFER, ident="0")], ) cred_offer.assign_thread_id(thread_id) cred_request = V20CredRequest( @@ -1210,22 +1215,22 @@ async def test_store_credential_holder_store_indy_error(self): V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + requests_attach=[AttachDecorator.data_base64(ANONCREDS_CRED_REQ, ident="0")], ) cred_issue = V20CredIssue( formats=[ V20CredFormat( attach_id="0", format_=ATTACHMENT_FORMAT[CRED_20_ISSUE][ - V20CredFormat.Format.INDY.api + V20CredFormat.Format.ANONCREDS.api ], ) ], - credentials_attach=[AttachDecorator.data_base64(INDY_CRED, ident="0")], + credentials_attach=[AttachDecorator.data_base64(ANONCREDS_CRED, ident="0")], ) stored_cx_rec = V20CredExRecord( diff --git a/acapy_agent/protocols/issue_credential/v2_0/formats/indy/handler.py b/acapy_agent/protocols/issue_credential/v2_0/formats/indy/handler.py index ecadd98539..19a2857cf3 100644 --- a/acapy_agent/protocols/issue_credential/v2_0/formats/indy/handler.py +++ b/acapy_agent/protocols/issue_credential/v2_0/formats/indy/handler.py @@ -7,6 +7,7 @@ from marshmallow import RAISE +from ......askar.profile_anon import AskarAnoncredsProfile from ......cache.base import BaseCache from ......core.profile import Profile from ......indy.holder import IndyHolder, IndyHolderError @@ -14,7 +15,6 @@ from ......indy.models.cred import IndyCredentialSchema from ......indy.models.cred_abstract import IndyCredAbstractSchema from ......indy.models.cred_request import IndyCredRequestSchema -from ......ledger.base import BaseLedger from ......ledger.multiple_ledger.ledger_requests_executor import ( GET_CRED_DEF, GET_SCHEMA, @@ -105,10 +105,6 @@ async def get_detail_record(self, cred_ex_id: str) -> V20CredExRecordIndy: session, cred_ex_id ) - # Temporary shim while the new anoncreds library integration is in progress - if self.anoncreds_handler: - return await self.anoncreds_handler.get_detail_record(cred_ex_id) - if len(records) > 1: LOGGER.warning( "Cred ex id %s has %d %s detail records: should be 1", @@ -140,9 +136,6 @@ def get_format_identifier(self, message_type: str) -> str: str: Issue credential attachment format identifier """ - # Temporary shim while the new anoncreds library integration is in progress - if self.anoncreds_handler: - return self.anoncreds_handler.get_format_identifier(message_type) return ATTACHMENT_FORMAT[message_type][IndyCredFormatHandler.format.api] @@ -162,9 +155,6 @@ def get_format_data(self, message_type: str, data: dict) -> CredFormatAttachment CredFormatAttachment: Credential format and attachment data objects """ - # Temporary shim while the new anoncreds library integration is in progress - if self.anoncreds_handler: - return self.anoncreds_handler.get_format_data(message_type, data) return ( V20CredFormat( @@ -192,7 +182,7 @@ async def create_proposal( self, cred_ex_record: V20CredExRecord, proposal_data: Mapping[str, str] ) -> Tuple[V20CredFormat, AttachDecorator]: """Create indy credential proposal.""" - # Temporary shim while the new anoncreds library integration is in progress + # Create the proposal with the anoncreds handler if agent is anoncreds capable if self.anoncreds_handler: return await self.anoncreds_handler.create_proposal( cred_ex_record, @@ -217,12 +207,12 @@ async def create_offer( ) -> CredFormatAttachment: """Create indy credential offer.""" - # Temporary shim while the new anoncreds library integration is in progress - if self.anoncreds_handler: - return await self.anoncreds_handler.create_offer(cred_proposal_message) + if isinstance(self.profile, AskarAnoncredsProfile): + raise V20CredFormatError( + "This issuer is anoncreds capable. Please use the anonreds format." + ) issuer = self.profile.inject(IndyIssuer) - ledger = self.profile.inject(BaseLedger) cache = self.profile.inject_or(BaseCache) cred_def_id = await self._match_sent_cred_def_id( @@ -275,11 +265,38 @@ async def receive_offer( ) -> None: """Receive indy credential offer.""" + async def create_cred_request_result(self, cred_offer, holder_did, cred_def_id): + """Create credential request result.""" + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) + ledger = ( + await ledger_exec_inst.get_ledger_for_identifier( + cred_def_id, + txn_record_type=GET_CRED_DEF, + ) + )[1] + async with ledger: + cred_def = await ledger.get_credential_definition(cred_def_id) + + holder = self.profile.inject(IndyHolder) + request_json, metadata_json = await holder.create_credential_request( + cred_offer, cred_def, holder_did + ) + + return { + "request": json.loads(request_json), + "metadata": json.loads(metadata_json), + } + async def create_request( self, cred_ex_record: V20CredExRecord, request_data: Optional[Mapping] = None ) -> CredFormatAttachment: """Create indy credential request.""" - # Temporary shim while the new anoncreds library integration is in progress + + # Create the request with the anoncreds handler if agent is anoncreds capable if self.anoncreds_handler: return await self.anoncreds_handler.create_request( cred_ex_record, @@ -302,31 +319,6 @@ async def create_request( nonce = cred_offer["nonce"] cred_def_id = cred_offer["cred_def_id"] - async def _create(): - multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) - if multitenant_mgr: - ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) - else: - ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) - ledger = ( - await ledger_exec_inst.get_ledger_for_identifier( - cred_def_id, - txn_record_type=GET_CRED_DEF, - ) - )[1] - async with ledger: - cred_def = await ledger.get_credential_definition(cred_def_id) - - holder = self.profile.inject(IndyHolder) - request_json, metadata_json = await holder.create_credential_request( - cred_offer, cred_def, holder_did - ) - - return { - "request": json.loads(request_json), - "metadata": json.loads(metadata_json), - } - cache_key = f"credential_request::{cred_def_id}::{holder_did}::{nonce}" cred_req_result = None cache = self.profile.inject_or(BaseCache) @@ -335,10 +327,14 @@ async def _create(): if entry.result: cred_req_result = entry.result else: - cred_req_result = await _create() + cred_req_result = await self.create_cred_request_result( + cred_offer, holder_did, cred_def_id + ) await entry.set_result(cred_req_result, 3600) if not cred_req_result: - cred_req_result = await _create() + cred_req_result = await self.create_cred_request_result( + cred_offer, holder_did, cred_def_id + ) detail_record = V20CredExRecordIndy( cred_ex_id=cred_ex_record.cred_ex_id, @@ -354,7 +350,7 @@ async def receive_request( self, cred_ex_record: V20CredExRecord, cred_request_message: V20CredRequest ) -> None: """Receive indy credential request.""" - # Temporary shim while the new anoncreds library integration is in progress + # Receive the request with the anoncreds handler if agent is anoncreds capable if self.anoncreds_handler: return await self.anoncreds_handler.receive_request( cred_ex_record, @@ -445,9 +441,11 @@ async def issue_credential( await self._check_uniqueness(cred_ex_record.cred_ex_id) cred_offer = cred_ex_record.cred_offer.attachment(IndyCredFormatHandler.format) + from ..anoncreds.handler import AnonCredsCredFormatHandler + cred_request = cred_ex_record.cred_request.attachment( IndyCredFormatHandler.format - ) + ) or cred_ex_record.cred_request.attachment(AnonCredsCredFormatHandler.format) cred_values = cred_ex_record.cred_offer.credential_preview.attr_dict(decode=False) schema_id = cred_offer["schema_id"] cred_def_id = cred_offer["cred_def_id"] @@ -517,10 +515,14 @@ async def store_credential( ) -> None: """Store indy credential.""" # Temporary shim while the new anoncreds library integration is in progress - if self.anoncreds_handler: + if hasattr(self, "anoncreds_handler") and self.anoncreds_handler: return await self.anoncreds_handler.store_credential(cred_ex_record, cred_id) - cred = cred_ex_record.cred_issue.attachment(IndyCredFormatHandler.format) + from ..anoncreds.handler import AnonCredsCredFormatHandler + + cred = cred_ex_record.cred_issue.attachment( + IndyCredFormatHandler.format + ) or cred_ex_record.cred_issue.attachment(AnonCredsCredFormatHandler.format) rev_reg_def = None multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) diff --git a/acapy_agent/protocols/issue_credential/v2_0/formats/ld_proof/models/cred_detail_options.py b/acapy_agent/protocols/issue_credential/v2_0/formats/ld_proof/models/cred_detail_options.py index 9d74382d8d..7a9b1ca549 100644 --- a/acapy_agent/protocols/issue_credential/v2_0/formats/ld_proof/models/cred_detail_options.py +++ b/acapy_agent/protocols/issue_credential/v2_0/formats/ld_proof/models/cred_detail_options.py @@ -6,8 +6,8 @@ from .......messaging.models.base import BaseModel, BaseModelSchema from .......messaging.valid import ( - INDY_ISO8601_DATETIME_EXAMPLE, - INDY_ISO8601_DATETIME_VALIDATE, + ISO8601_DATETIME_EXAMPLE, + ISO8601_DATETIME_VALIDATE, UUID4_EXAMPLE, ) @@ -109,13 +109,13 @@ class Meta: created = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": ( "The date and time of the proof (with a maximum accuracy in seconds)." " Defaults to current system time" ), - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) diff --git a/acapy_agent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py b/acapy_agent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py index ae8f23fa69..3ff75a5119 100644 --- a/acapy_agent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py +++ b/acapy_agent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py @@ -9,11 +9,11 @@ from .......anoncreds.holder import AnonCredsHolder, AnonCredsHolderError from .......anoncreds.issuer import AnonCredsIssuer -from .......anoncreds.models.anoncreds_cred_def import ( +from .......anoncreds.models.credential_definition import ( CredDef, GetCredDefResult, ) -from .......anoncreds.models.anoncreds_revocation import ( +from .......anoncreds.models.revocation import ( GetRevRegDefResult, RevRegDef, ) diff --git a/acapy_agent/protocols/issue_credential/v2_0/manager.py b/acapy_agent/protocols/issue_credential/v2_0/manager.py index 068f94a4bd..124e832c47 100644 --- a/acapy_agent/protocols/issue_credential/v2_0/manager.py +++ b/acapy_agent/protocols/issue_credential/v2_0/manager.py @@ -591,8 +591,14 @@ async def receive_credential( ] handled_formats = [] - # check that we didn't receive any formats not present in the request - if set(issue_formats) - set(req_formats): + def _check_formats(): + """Allow indy issue fomat and anoncreds req format or matching formats.""" + return ( + issue_formats == [V20CredFormat.Format.INDY] + and req_formats == [V20CredFormat.Format.ANONCREDS] + ) or len(set(issue_formats) - set(req_formats)) == 0 + + if not _check_formats(): raise V20CredManagerError( "Received issue credential format(s) not present in credential " f"request: {set(issue_formats) - set(req_formats)}" diff --git a/acapy_agent/protocols/issue_credential/v2_0/message_types.py b/acapy_agent/protocols/issue_credential/v2_0/message_types.py index 668d2b7332..e34ce0385d 100644 --- a/acapy_agent/protocols/issue_credential/v2_0/message_types.py +++ b/acapy_agent/protocols/issue_credential/v2_0/message_types.py @@ -37,21 +37,25 @@ # Format specifications ATTACHMENT_FORMAT = { CRED_20_PROPOSAL: { + V20CredFormat.Format.ANONCREDS.api: "anoncreds/cred-filter@v2.0", V20CredFormat.Format.INDY.api: "hlindy/cred-filter@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc-detail@v1.0", V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc@v0.1", }, CRED_20_OFFER: { + V20CredFormat.Format.ANONCREDS.api: "anoncreds/cred-abstract@v2.0", V20CredFormat.Format.INDY.api: "hlindy/cred-abstract@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc-detail@v1.0", V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc-offer@v0.1", }, CRED_20_REQUEST: { + V20CredFormat.Format.ANONCREDS.api: "anoncreds/cred-req@v2.0", V20CredFormat.Format.INDY.api: "hlindy/cred-req@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc-detail@v1.0", V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc-request@v0.1", }, CRED_20_ISSUE: { + V20CredFormat.Format.ANONCREDS.api: "anoncreds/cred@v2.0", V20CredFormat.Format.INDY.api: "hlindy/cred@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc@v1.0", V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc@v0.1", diff --git a/acapy_agent/protocols/issue_credential/v2_0/messages/cred_format.py b/acapy_agent/protocols/issue_credential/v2_0/messages/cred_format.py index 0574d4736d..589dfcf579 100644 --- a/acapy_agent/protocols/issue_credential/v2_0/messages/cred_format.py +++ b/acapy_agent/protocols/issue_credential/v2_0/messages/cred_format.py @@ -11,6 +11,7 @@ from .....messaging.models.base import BaseModel, BaseModelSchema from .....messaging.valid import UUID4_EXAMPLE from .....utils.classloader import DeferLoad +from ..models.detail.anoncreds import V20CredExRecordAnoncreds from ..models.detail.indy import V20CredExRecordIndy from ..models.detail.ld_proof import V20CredExRecordLDProof @@ -31,6 +32,14 @@ class Meta: class Format(Enum): """Attachment format.""" + ANONCREDS = FormatSpec( + "anoncreds/", + V20CredExRecordAnoncreds, + DeferLoad( + "acapy_agent.protocols.issue_credential.v2_0" + ".formats.anoncreds.handler.AnonCredsCredFormatHandler" + ), + ) INDY = FormatSpec( "hlindy/", V20CredExRecordIndy, @@ -39,23 +48,6 @@ class Format(Enum): ".formats.indy.handler.IndyCredFormatHandler" ), ) - """ - Once we switch to anoncreds this will replace the above INDY definition. - - In the meantime there are some hardcoded references in the - "...formats.indy.handler.IndyCredFormatHandler" class. - - :: - - INDY = FormatSpec( - "hlindy/", - V20CredExRecordIndy, - DeferLoad( - "acapy_agent.protocols.issue_credential.v2_0" - ".formats.anoncreds.handler.AnonCredsCredFormatHandler" - ), - ) - """ LD_PROOF = FormatSpec( "aries/", V20CredExRecordLDProof, @@ -97,7 +89,9 @@ def aries(self) -> str: return self.value.aries @property - def detail(self) -> Union[V20CredExRecordIndy, V20CredExRecordLDProof]: + def detail( + self, + ) -> Union[V20CredExRecordIndy, V20CredExRecordLDProof, V20CredExRecordAnoncreds]: """Accessor for credential exchange detail class.""" return self.value.detail diff --git a/acapy_agent/protocols/issue_credential/v2_0/models/detail/anoncreds.py b/acapy_agent/protocols/issue_credential/v2_0/models/detail/anoncreds.py new file mode 100644 index 0000000000..f683ff68ca --- /dev/null +++ b/acapy_agent/protocols/issue_credential/v2_0/models/detail/anoncreds.py @@ -0,0 +1,131 @@ +"""Anoncreds specific credential exchange information with non-secrets storage.""" + +from typing import Any, Mapping, Optional, Sequence + +from marshmallow import EXCLUDE, fields + +from ......core.profile import ProfileSession +from ......messaging.models.base_record import BaseRecord, BaseRecordSchema +from ......messaging.valid import ( + ANONCREDS_CRED_DEF_ID_EXAMPLE, + ANONCREDS_REV_REG_ID_EXAMPLE, + UUID4_EXAMPLE, +) +from .. import UNENCRYPTED_TAGS + + +class V20CredExRecordAnoncreds(BaseRecord): + """Credential exchange anoncreds detail record.""" + + class Meta: + """V20CredExRecordAnoncreds metadata.""" + + schema_class = "V20CredExRecordAnoncredsSchema" + + RECORD_ID_NAME = "cred_ex_anoncreds_id" + RECORD_TYPE = "anoncreds_cred_ex_v20" + TAG_NAMES = {"~cred_ex_id"} if UNENCRYPTED_TAGS else {"cred_ex_id"} + RECORD_TOPIC = "issue_credential_v2_0_anoncreds" + + def __init__( + self, + cred_ex_anoncreds_id: Optional[str] = None, + *, + cred_ex_id: Optional[str] = None, + cred_id_stored: Optional[str] = None, + cred_request_metadata: Optional[Mapping] = None, + rev_reg_id: Optional[str] = None, + cred_rev_id: Optional[str] = None, + **kwargs, + ): + """Initialize anoncreds credential exchange record details.""" + super().__init__(cred_ex_anoncreds_id, **kwargs) + + self.cred_ex_id = cred_ex_id + self.cred_id_stored = cred_id_stored + self.cred_request_metadata = cred_request_metadata + self.rev_reg_id = rev_reg_id + self.cred_rev_id = cred_rev_id + + @property + def cred_ex_anoncreds_id(self) -> str: + """Accessor for the ID associated with this exchange.""" + return self._id + + @property + def record_value(self) -> dict: + """Accessor for the JSON record value generated for this credential exchange.""" + return { + prop: getattr(self, prop) + for prop in ( + "cred_id_stored", + "cred_request_metadata", + "rev_reg_id", + "cred_rev_id", + ) + } + + @classmethod + async def query_by_cred_ex_id( + cls, + session: ProfileSession, + cred_ex_id: str, + ) -> Sequence["V20CredExRecordAnoncreds"]: + """Retrieve credential exchange anoncreds detail record(s) by its cred ex id.""" + return await cls.query( + session=session, + tag_filter={"cred_ex_id": cred_ex_id}, + ) + + def __eq__(self, other: Any) -> bool: + """Comparison between records.""" + return super().__eq__(other) + + +class V20CredExRecordAnoncredsSchema(BaseRecordSchema): + """Credential exchange anoncreds detail record detail schema.""" + + class Meta: + """Credential exchange anoncreds detail record schema metadata.""" + + model_class = V20CredExRecordAnoncreds + unknown = EXCLUDE + + cred_ex_anoncreds_id = fields.Str( + required=False, + metadata={"description": "Record identifier", "example": UUID4_EXAMPLE}, + ) + cred_ex_id = fields.Str( + required=False, + metadata={ + "description": "Corresponding v2.0 credential exchange record identifier", + "example": UUID4_EXAMPLE, + }, + ) + cred_id_stored = fields.Str( + required=False, + metadata={ + "description": "Credential identifier stored in wallet", + "example": UUID4_EXAMPLE, + }, + ) + cred_request_metadata = fields.Dict( + required=False, + metadata={"description": "Credential request metadata for anoncreds holder"}, + ) + rev_reg_id = fields.Str( + required=False, + metadata={ + "description": "Revocation registry identifier", + "example": ANONCREDS_REV_REG_ID_EXAMPLE, + }, + ) + cred_rev_id = fields.Str( + required=False, + metadata={ + "description": ( + "Credential revocation identifier within revocation registry" + ), + "example": ANONCREDS_CRED_DEF_ID_EXAMPLE, + }, + ) diff --git a/acapy_agent/protocols/issue_credential/v2_0/routes.py b/acapy_agent/protocols/issue_credential/v2_0/routes.py index 2fdb4233e4..e808de683d 100644 --- a/acapy_agent/protocols/issue_credential/v2_0/routes.py +++ b/acapy_agent/protocols/issue_credential/v2_0/routes.py @@ -28,14 +28,16 @@ from ....messaging.models.openapi import OpenAPISchema from ....messaging.models.paginated_query import PaginatedQuerySchema, get_limit_offset from ....messaging.valid import ( + ANONCREDS_DID_EXAMPLE, + ANONCREDS_SCHEMA_ID_EXAMPLE, INDY_CRED_DEF_ID_EXAMPLE, INDY_CRED_DEF_ID_VALIDATE, INDY_DID_EXAMPLE, INDY_DID_VALIDATE, INDY_SCHEMA_ID_EXAMPLE, INDY_SCHEMA_ID_VALIDATE, - INDY_VERSION_EXAMPLE, - INDY_VERSION_VALIDATE, + MAJOR_MINOR_VERSION_EXAMPLE, + MAJOR_MINOR_VERSION_VALIDATE, UUID4_EXAMPLE, UUID4_VALIDATE, CHEQD_DID_VALIDATE, @@ -138,6 +140,36 @@ class V20CredStoreRequestSchema(OpenAPISchema): credential_id = fields.Str(required=False) +class V20CredFilterAnoncredsSchema(OpenAPISchema): + """Anoncreds credential filtration criteria.""" + + cred_def_id = fields.Str( + required=False, + metadata={ + "description": "Credential definition identifier", + "example": ANONCREDS_DID_EXAMPLE, + }, + ) + schema_id = fields.Str( + required=False, + metadata={ + "description": "Schema identifier", + "example": ANONCREDS_SCHEMA_ID_EXAMPLE, + }, + ) + issuer_id = fields.Str( + required=False, + metadata={ + "description": "Credential issuer DID", + "example": ANONCREDS_DID_EXAMPLE, + }, + ) + epoch = fields.Str( + required=False, + metadata={"description": "Credential epoch time", "example": "2021-08-24"}, + ) + + class V20CredFilterIndySchema(OpenAPISchema): """Indy credential filtration criteria.""" @@ -168,8 +200,11 @@ class V20CredFilterIndySchema(OpenAPISchema): ) schema_version = fields.Str( required=False, - validate=INDY_VERSION_VALIDATE, - metadata={"description": "Schema version", "example": INDY_VERSION_EXAMPLE}, + validate=MAJOR_MINOR_VERSION_VALIDATE, + metadata={ + "description": "Schema version", + "example": MAJOR_MINOR_VERSION_EXAMPLE, + }, ) issuer_did = fields.Str( required=False, @@ -208,8 +243,11 @@ class V20CredFilterVCDISchema(OpenAPISchema): ) schema_version = fields.Str( required=False, - validate=INDY_VERSION_VALIDATE, - metadata={"description": "Schema version", "example": INDY_VERSION_EXAMPLE}, + validate=MAJOR_MINOR_VERSION_VALIDATE, + metadata={ + "description": "Schema version", + "example": MAJOR_MINOR_VERSION_EXAMPLE, + }, ) issuer_did = fields.Str( required=False, @@ -221,6 +259,11 @@ class V20CredFilterVCDISchema(OpenAPISchema): class V20CredFilterSchema(OpenAPISchema): """Credential filtration criteria.""" + anoncreds = fields.Nested( + V20CredFilterAnoncredsSchema, + required=False, + metadata={"description": "Credential filter for anoncreds"}, + ) indy = fields.Nested( V20CredFilterIndySchema, required=False, @@ -952,11 +995,17 @@ async def _create_free_offer( ) cred_manager = V20CredManager(profile) - (cred_ex_record, cred_offer_message) = await cred_manager.create_offer( - cred_ex_record, - comment=comment, - replacement_id=replacement_id, - ) + try: + (cred_ex_record, cred_offer_message) = await cred_manager.create_offer( + cred_ex_record, + comment=comment, + replacement_id=replacement_id, + ) + except ValueError as err: + LOGGER.exception(f"Error creating credential offer: {err}") + async with profile.session() as session: + await cred_ex_record.save_error_state(session, reason=err) + raise web.HTTPBadRequest(reason=err) return (cred_ex_record, cred_offer_message) diff --git a/acapy_agent/protocols/issue_credential/v2_0/tests/test_routes.py b/acapy_agent/protocols/issue_credential/v2_0/tests/test_routes.py index 5ca0854086..1359057136 100644 --- a/acapy_agent/protocols/issue_credential/v2_0/tests/test_routes.py +++ b/acapy_agent/protocols/issue_credential/v2_0/tests/test_routes.py @@ -149,9 +149,10 @@ async def test_credential_exchange_retrieve(self): mock_handler.return_value.get_detail_record = mock.CoroutineMock( side_effect=[ - mock.MagicMock( # indy + mock.MagicMock( # anoncreds serialize=mock.MagicMock(return_value={"...": "..."}) ), + None, # indy None, # ld_proof None, # vc_di ] @@ -162,7 +163,8 @@ async def test_credential_exchange_retrieve(self): mock_response.assert_called_once_with( { "cred_ex_record": mock_cx_rec.serialize.return_value, - "indy": {"...": "..."}, + "anoncreds": {"...": "..."}, + "indy": None, "ld_proof": None, "vc_di": None, } @@ -185,6 +187,9 @@ async def test_credential_exchange_retrieve_indy_ld_proof(self): mock_handler.return_value.get_detail_record = mock.CoroutineMock( side_effect=[ + mock.MagicMock( # anoncreds + serialize=mock.MagicMock(return_value={"anon": "creds"}) + ), mock.MagicMock( # indy serialize=mock.MagicMock(return_value={"in": "dy"}) ), @@ -202,6 +207,7 @@ async def test_credential_exchange_retrieve_indy_ld_proof(self): mock_response.assert_called_once_with( { "cred_ex_record": mock_cx_rec.serialize.return_value, + "anoncreds": {"anon": "creds"}, "indy": {"in": "dy"}, "ld_proof": {"ld": "proof"}, "vc_di": {"vc": "di"}, @@ -1230,9 +1236,10 @@ async def test_credential_exchange_issue(self): mock_handler.return_value.get_detail_record = mock.CoroutineMock( side_effect=[ - mock.MagicMock( # indy + mock.MagicMock( # anoncreds serialize=mock.MagicMock(return_value={"...": "..."}) ), + None, None, # ld_proof None, # vc_di ] @@ -1248,7 +1255,8 @@ async def test_credential_exchange_issue(self): mock_response.assert_called_once_with( { "cred_ex_record": mock_cx_rec.serialize.return_value, - "indy": {"...": "..."}, + "anoncreds": {"...": "..."}, + "indy": None, "ld_proof": None, "vc_di": None, } @@ -1277,7 +1285,8 @@ async def test_credential_exchange_issue_vcdi(self): mock_handler.return_value.get_detail_record = mock.CoroutineMock( side_effect=[ - None, + None, # anoncreds + None, # indy None, # ld_proof mock.MagicMock( # indy serialize=mock.MagicMock(return_value={"...": "..."}) @@ -1295,6 +1304,7 @@ async def test_credential_exchange_issue_vcdi(self): mock_response.assert_called_once_with( { "cred_ex_record": mock_cx_rec.serialize.return_value, + "anoncreds": None, "indy": None, "ld_proof": None, "vc_di": {"...": "..."}, @@ -1498,6 +1508,7 @@ async def test_credential_exchange_store(self): ) mock_handler.return_value.get_detail_record = mock.CoroutineMock( side_effect=[ + None, # anoncreds mock.MagicMock( # indy serialize=mock.MagicMock(return_value={"...": "..."}) ), @@ -1519,6 +1530,7 @@ async def test_credential_exchange_store(self): mock_response.assert_called_once_with( { "cred_ex_record": mock_cx_rec.serialize.return_value, + "anoncreds": None, "indy": {"...": "..."}, "ld_proof": None, "vc_di": None, @@ -1575,6 +1587,7 @@ async def test_credential_exchange_store_bad_cred_id_json(self): mock_response.assert_called_once_with( { "cred_ex_record": mock_cx_rec.serialize.return_value, + "anoncreds": None, "indy": {"...": "..."}, "ld_proof": None, "vc_di": None, @@ -1678,6 +1691,7 @@ async def test_credential_exchange_store_x(self): ) mock_handler.return_value.get_detail_record = mock.CoroutineMock( side_effect=[ + None, # anoncreds mock.MagicMock( # indy serialize=mock.MagicMock(return_value={"...": "..."}) ), diff --git a/acapy_agent/protocols/present_proof/anoncreds/pres_exch_handler.py b/acapy_agent/protocols/present_proof/anoncreds/pres_exch_handler.py index 0052a78873..d1d73f8a72 100644 --- a/acapy_agent/protocols/present_proof/anoncreds/pres_exch_handler.py +++ b/acapy_agent/protocols/present_proof/anoncreds/pres_exch_handler.py @@ -1,4 +1,4 @@ -"""Utilities for dif presentation exchange attachment.""" +"""Utilities for anoncreds presentation exchange attachment.""" import json import logging @@ -6,14 +6,15 @@ from typing import Dict, Optional, Tuple, Union from ....anoncreds.holder import AnonCredsHolder, AnonCredsHolderError -from ....anoncreds.models.anoncreds_cred_def import CredDef -from ....anoncreds.models.anoncreds_revocation import RevRegDef -from ....anoncreds.models.anoncreds_schema import AnonCredsSchema +from ....anoncreds.models.credential_definition import CredDef +from ....anoncreds.models.revocation import RevRegDef +from ....anoncreds.models.schema import AnonCredsSchema +from ....anoncreds.models.utils import extract_non_revocation_intervals_from_proof_request from ....anoncreds.registry import AnonCredsRegistry from ....anoncreds.revocation import AnonCredsRevocation +from ....askar.profile_anon import AskarAnoncredsProfile from ....core.error import BaseError from ....core.profile import Profile -from ....indy.models.xform import indy_proof_req2non_revoc_intervals from ..v1_0.models.presentation_exchange import V10PresentationExchange from ..v2_0.messages.pres_format import V20PresFormat from ..v2_0.models.pres_exchange import V20PresExRecord @@ -22,7 +23,7 @@ class AnonCredsPresExchHandlerError(BaseError): - """Base class for Indy Presentation Exchange related errors.""" + """Base class for Anoncreds Presentation Exchange related errors.""" class AnonCredsPresExchHandler: @@ -39,7 +40,9 @@ def __init__( def _extract_proof_request(self, pres_ex_record): if isinstance(pres_ex_record, V20PresExRecord): - return pres_ex_record.pres_request.attachment(V20PresFormat.Format.INDY) + return pres_ex_record.pres_request.attachment( + V20PresFormat.Format.ANONCREDS + ) or pres_ex_record.pres_request.attachment(V20PresFormat.Format.INDY) elif isinstance(pres_ex_record, V10PresentationExchange): return pres_ex_record._presentation_request.ser @@ -229,10 +232,23 @@ async def return_presentation( pres_ex_record: Union[V10PresentationExchange, V20PresExRecord], requested_credentials: Optional[dict] = None, ) -> dict: - """Return Indy proof request as dict.""" + """Return Anoncreds proof request as dict.""" + + # If not anoncreds capable, try to use indy handler. This should be removed when + # indy filter is completely retired + if not isinstance(self._profile, AskarAnoncredsProfile): + from ..indy.pres_exch_handler import IndyPresExchHandler + + handler = IndyPresExchHandler(self._profile) + return await handler.return_presentation( + pres_ex_record, requested_credentials + ) + requested_credentials = requested_credentials or {} proof_request = self._extract_proof_request(pres_ex_record) - non_revoc_intervals = indy_proof_req2non_revoc_intervals(proof_request) + non_revoc_intervals = extract_non_revocation_intervals_from_proof_request( + proof_request + ) requested_referents = self._get_requested_referents( proof_request, requested_credentials, non_revoc_intervals @@ -253,12 +269,12 @@ async def return_presentation( self._set_timestamps(requested_credentials, requested_referents) - indy_proof_json = await self.holder.create_presentation( + proof_json = await self.holder.create_presentation( proof_request, requested_credentials, schemas, cred_defs, revocation_states, ) - indy_proof = json.loads(indy_proof_json) - return indy_proof + proof = json.loads(proof_json) + return proof diff --git a/acapy_agent/protocols/present_proof/indy/pres_exch_handler.py b/acapy_agent/protocols/present_proof/indy/pres_exch_handler.py index 9c510afc9a..b62701ced1 100644 --- a/acapy_agent/protocols/present_proof/indy/pres_exch_handler.py +++ b/acapy_agent/protocols/present_proof/indy/pres_exch_handler.py @@ -55,6 +55,13 @@ async def return_presentation( proof_request = pres_ex_record.pres_request.attachment( V20PresFormat.Format.INDY ) + # If indy filter fails try anoncreds filter format. This is for a + # non-anoncreds agent that gets a anoncreds format proof request and + # should removed when indy format is fully retired. + if not proof_request: + proof_request = pres_ex_record.pres_request.attachment( + V20PresFormat.Format.ANONCREDS + ) elif isinstance(pres_ex_record, V10PresentationExchange): proof_request = pres_ex_record._presentation_request.ser non_revoc_intervals = indy_proof_req2non_revoc_intervals(proof_request) diff --git a/acapy_agent/protocols/present_proof/v1_0/tests/test_routes.py b/acapy_agent/protocols/present_proof/v1_0/tests/test_routes.py index afe923e8be..5c55474765 100644 --- a/acapy_agent/protocols/present_proof/v1_0/tests/test_routes.py +++ b/acapy_agent/protocols/present_proof/v1_0/tests/test_routes.py @@ -4,8 +4,10 @@ from marshmallow import ValidationError from .....admin.request_context import AdminRequestContext +from .....anoncreds.models.presentation_request import ( + AnoncredsPresentationReqAttrSpecSchema, +) from .....indy.holder import IndyHolder -from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....indy.verifier import IndyVerifier from .....ledger.base import BaseLedger from .....storage.error import StorageNotFoundError @@ -35,7 +37,7 @@ async def asyncSetUp(self): ) async def test_validate_proof_req_attr_spec(self): - aspec = IndyProofReqAttrSpecSchema() + aspec = AnoncredsPresentationReqAttrSpecSchema() aspec.validate_fields({"name": "attr0"}) aspec.validate_fields( { diff --git a/acapy_agent/protocols/present_proof/v2_0/formats/anoncreds/handler.py b/acapy_agent/protocols/present_proof/v2_0/formats/anoncreds/handler.py index d5618f4a50..2c19d13443 100644 --- a/acapy_agent/protocols/present_proof/v2_0/formats/anoncreds/handler.py +++ b/acapy_agent/protocols/present_proof/v2_0/formats/anoncreds/handler.py @@ -1,4 +1,4 @@ -"""V2.0 present-proof indy presentation-exchange format handler.""" +"""V2.0 present-proof anoncreds presentation-exchange format handler.""" import json import logging @@ -7,12 +7,12 @@ from marshmallow import RAISE from ......anoncreds.holder import AnonCredsHolder +from ......anoncreds.models.predicate import Predicate +from ......anoncreds.models.presentation_request import AnoncredsPresentationRequestSchema +from ......anoncreds.models.proof import AnoncredsProofSchema +from ......anoncreds.models.utils import get_requested_creds_from_proof_request_preview from ......anoncreds.util import generate_pr_nonce from ......anoncreds.verifier import AnonCredsVerifier -from ......indy.models.predicate import Predicate -from ......indy.models.proof import IndyProofSchema -from ......indy.models.proof_request import IndyProofRequestSchema -from ......indy.models.xform import indy_proof_req_preview2indy_requested_creds from ......messaging.decorators.attach_decorator import AttachDecorator from ......messaging.util import canon from ....anoncreds.pres_exch_handler import AnonCredsPresExchHandler @@ -33,7 +33,7 @@ class AnonCredsPresExchangeHandler(V20PresFormatHandler): """Anoncreds presentation format handler.""" - format = V20PresFormat.Format.INDY + format = V20PresFormat.Format.ANONCREDS @classmethod def validate_fields(cls, message_type: str, attachment_data: Mapping): @@ -55,9 +55,9 @@ def validate_fields(cls, message_type: str, attachment_data: Mapping): """ mapping = { - PRES_20_REQUEST: IndyProofRequestSchema, - PRES_20_PROPOSAL: IndyProofRequestSchema, - PRES_20: IndyProofSchema, + PRES_20_REQUEST: AnoncredsPresentationRequestSchema, + PRES_20_PROPOSAL: AnoncredsPresentationRequestSchema, + PRES_20: AnoncredsProofSchema, } # Get schema class @@ -109,20 +109,20 @@ async def create_bound_request( A tuple (updated presentation exchange record, presentation request message) """ - indy_proof_request = pres_ex_record.pres_proposal.attachment( + proof_request = pres_ex_record.pres_proposal.attachment( AnonCredsPresExchangeHandler.format ) if request_data: - indy_proof_request["name"] = request_data.get("name", "proof-request") - indy_proof_request["version"] = request_data.get("version", "1.0") - indy_proof_request["nonce"] = ( + proof_request["name"] = request_data.get("name", "proof-request") + proof_request["version"] = request_data.get("version", "1.0") + proof_request["nonce"] = ( request_data.get("nonce") or await generate_pr_nonce() ) else: - indy_proof_request["name"] = "proof-request" - indy_proof_request["version"] = "1.0" - indy_proof_request["nonce"] = await generate_pr_nonce() - return self.get_format_data(PRES_20_REQUEST, indy_proof_request) + proof_request["name"] = "proof-request" + proof_request["version"] = "1.0" + proof_request["nonce"] = await generate_pr_nonce() + return self.get_format_data(PRES_20_REQUEST, proof_request) async def create_pres( self, @@ -131,45 +131,58 @@ async def create_pres( ) -> Tuple[V20PresFormat, AttachDecorator]: """Create a presentation.""" requested_credentials = {} + + # This is used for the fallback to indy format + from ..indy.handler import IndyPresExchangeHandler + if not request_data: try: proof_request = pres_ex_record.pres_request - indy_proof_request = proof_request.attachment( + # Fall back to indy format should be removed when indy format retired + proof_request = proof_request.attachment( AnonCredsPresExchangeHandler.format - ) - requested_credentials = await indy_proof_req_preview2indy_requested_creds( - indy_proof_request, - preview=None, - holder=AnonCredsHolder(self._profile), + ) or proof_request.attachment(IndyPresExchangeHandler.format) + requested_credentials = ( + await get_requested_creds_from_proof_request_preview( + proof_request, + holder=AnonCredsHolder(self._profile), + ) ) except ValueError as err: LOGGER.warning(f"{err}") - raise V20PresFormatHandlerError( - f"No matching Indy credentials found: {err}" - ) + raise V20PresFormatHandlerError(f"No matching credentials found: {err}") else: - if AnonCredsPresExchangeHandler.format.api in request_data: - indy_spec = request_data.get(AnonCredsPresExchangeHandler.format.api) + # Fall back to indy format should be removed when indy format retired + if ( + AnonCredsPresExchangeHandler.format.api in request_data + or IndyPresExchangeHandler.format.api in request_data + ): + spec = request_data.get( + AnonCredsPresExchangeHandler.format.api + ) or request_data.get(IndyPresExchangeHandler.format.api) requested_credentials = { - "self_attested_attributes": indy_spec["self_attested_attributes"], - "requested_attributes": indy_spec["requested_attributes"], - "requested_predicates": indy_spec["requested_predicates"], + "self_attested_attributes": spec["self_attested_attributes"], + "requested_attributes": spec["requested_attributes"], + "requested_predicates": spec["requested_predicates"], } - indy_handler = AnonCredsPresExchHandler(self._profile) - indy_proof = await indy_handler.return_presentation( + handler = AnonCredsPresExchHandler(self._profile) + presentation_proof = await handler.return_presentation( pres_ex_record=pres_ex_record, requested_credentials=requested_credentials, ) - return self.get_format_data(PRES_20, indy_proof) + return self.get_format_data(PRES_20, presentation_proof) async def receive_pres(self, message: V20Pres, pres_ex_record: V20PresExRecord): """Receive a presentation and check for presented values vs. proposal request.""" def _check_proof_vs_proposal(): """Check for bait and switch in presented values vs. proposal request.""" + from ..indy.handler import IndyPresExchangeHandler + + # Fall back to indy format should be removed when indy format retired proof_req = pres_ex_record.pres_request.attachment( AnonCredsPresExchangeHandler.format - ) + ) or pres_ex_record.pres_request.attachment(IndyPresExchangeHandler.format) # revealed attrs for reft, attr_spec in proof["requested_proof"]["revealed_attrs"].items(): @@ -256,7 +269,8 @@ def _check_proof_vs_proposal(): for req_restriction in req_restrictions: for k in list(req_restriction): # cannot modify en passant if k.startswith("attr::"): - req_restriction.pop(k) # let indy-sdk reject mismatch here + # let anoncreds-sdk reject mismatch here + req_restriction.pop(k) sub_proof_index = pred_spec["sub_proof_index"] for ge_proof in proof["proof"]["proofs"][sub_proof_index][ "primary_proof" @@ -313,10 +327,8 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: """ pres_request_msg = pres_ex_record.pres_request - indy_proof_request = pres_request_msg.attachment( - AnonCredsPresExchangeHandler.format - ) - indy_proof = pres_ex_record.pres.attachment(AnonCredsPresExchangeHandler.format) + proof_request = pres_request_msg.attachment(AnonCredsPresExchangeHandler.format) + proof = pres_ex_record.pres.attachment(AnonCredsPresExchangeHandler.format) verifier = AnonCredsVerifier(self._profile) ( @@ -324,13 +336,13 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: cred_defs, rev_reg_defs, rev_lists, - ) = await verifier.process_pres_identifiers(indy_proof["identifiers"]) + ) = await verifier.process_pres_identifiers(proof["identifiers"]) verifier = AnonCredsVerifier(self._profile) (verified, verified_msgs) = await verifier.verify_presentation( - indy_proof_request, - indy_proof, + proof_request, + proof, schemas, cred_defs, rev_reg_defs, diff --git a/acapy_agent/protocols/present_proof/v2_0/formats/handler.py b/acapy_agent/protocols/present_proof/v2_0/formats/handler.py index c4abf9d4c2..7e4e421cb6 100644 --- a/acapy_agent/protocols/present_proof/v2_0/formats/handler.py +++ b/acapy_agent/protocols/present_proof/v2_0/formats/handler.py @@ -1,4 +1,4 @@ -"""present-proof-v2 format handler - supports DIF and INDY.""" +"""present-proof-v2 format handler - supports ANONCREDS, DIF and INDY.""" import logging from abc import ABC, abstractclassmethod, abstractmethod diff --git a/acapy_agent/protocols/present_proof/v2_0/formats/indy/handler.py b/acapy_agent/protocols/present_proof/v2_0/formats/indy/handler.py index 306ed3b1af..bac3933919 100644 --- a/acapy_agent/protocols/present_proof/v2_0/formats/indy/handler.py +++ b/acapy_agent/protocols/present_proof/v2_0/formats/indy/handler.py @@ -88,9 +88,6 @@ def get_format_identifier(self, message_type: str) -> str: str: Issue credential attachment format identifier """ - # Temporary shim while the new anoncreds library integration is in progress - if self.anoncreds_handler: - return self.anoncreds_handler.get_format_identifier(message_type) return ATTACHMENT_FORMAT[message_type][IndyPresExchangeHandler.format.api] @@ -98,9 +95,6 @@ def get_format_data( self, message_type: str, data: dict ) -> Tuple[V20PresFormat, AttachDecorator]: """Get presentation format and attach objects for use in pres_ex messages.""" - # Temporary shim while the new anoncreds library integration is in progress - if self.anoncreds_handler: - return self.anoncreds_handler.get_format_data(message_type, data) return ( V20PresFormat( @@ -154,9 +148,12 @@ async def create_pres( request_data: Optional[dict] = None, ) -> Tuple[V20PresFormat, AttachDecorator]: """Create a presentation.""" - # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: - return await self.anoncreds_handler.create_pres(pres_ex_record, request_data) + return await self.anoncreds_handler.create_pres( + pres_ex_record, + request_data, + ) requested_credentials = {} if not request_data: @@ -349,8 +346,14 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: return await self.anoncreds_handler.verify_pres(pres_ex_record) pres_request_msg = pres_ex_record.pres_request - indy_proof_request = pres_request_msg.attachment(IndyPresExchangeHandler.format) - indy_proof = pres_ex_record.pres.attachment(IndyPresExchangeHandler.format) + + # The `or` anoncreds format is for the indy <--> anoncreds compatibility + indy_proof_request = pres_request_msg.attachment( + IndyPresExchangeHandler.format + ) or pres_request_msg.attachment(AnonCredsPresExchangeHandler.format) + indy_proof = pres_ex_record.pres.attachment( + IndyPresExchangeHandler.format + ) or pres_ex_record.pres.attachment(AnonCredsPresExchangeHandler.format) indy_handler = IndyPresExchHandler(self._profile) ( schemas, diff --git a/acapy_agent/protocols/present_proof/v2_0/message_types.py b/acapy_agent/protocols/present_proof/v2_0/message_types.py index 97b8f063fc..5654ef7d42 100644 --- a/acapy_agent/protocols/present_proof/v2_0/message_types.py +++ b/acapy_agent/protocols/present_proof/v2_0/message_types.py @@ -32,14 +32,17 @@ # Format specifications ATTACHMENT_FORMAT = { PRES_20_PROPOSAL: { + V20PresFormat.Format.ANONCREDS.api: "anoncreds/proof-req@v2.0", V20PresFormat.Format.INDY.api: "hlindy/proof-req@v2.0", V20PresFormat.Format.DIF.api: "dif/presentation-exchange/definitions@v1.0", }, PRES_20_REQUEST: { + V20PresFormat.Format.ANONCREDS.api: "anoncreds/proof-req@v2.0", V20PresFormat.Format.INDY.api: "hlindy/proof-req@v2.0", V20PresFormat.Format.DIF.api: "dif/presentation-exchange/definitions@v1.0", }, PRES_20: { + V20PresFormat.Format.ANONCREDS.api: "anoncreds/proof@v2.0", V20PresFormat.Format.INDY.api: "hlindy/proof@v2.0", V20PresFormat.Format.DIF.api: "dif/presentation-exchange/submission@v1.0", }, diff --git a/acapy_agent/protocols/present_proof/v2_0/messages/pres_format.py b/acapy_agent/protocols/present_proof/v2_0/messages/pres_format.py index 5c55384c86..e65e98644f 100644 --- a/acapy_agent/protocols/present_proof/v2_0/messages/pres_format.py +++ b/acapy_agent/protocols/present_proof/v2_0/messages/pres_format.py @@ -30,6 +30,13 @@ class Meta: class Format(Enum): """Attachment format.""" + ANONCREDS = FormatSpec( + "anoncreds/", + DeferLoad( + "acapy_agent.protocols.present_proof.v2_0" + ".formats.anoncreds.handler.AnonCredsPresExchangeHandler" + ), + ) INDY = FormatSpec( "hlindy/", DeferLoad( @@ -37,19 +44,6 @@ class Format(Enum): ".formats.indy.handler.IndyPresExchangeHandler" ), ) - """ - To make the switch from indy to anoncreds replace the above with the following. - - :: - - INDY = FormatSpec( - "hlindy/", - DeferLoad( - "acapy_agent.protocols.present_proof.v2_0" - ".formats.anoncreds.handler.AnonCredsPresExchangeHandler" - ), - ) - """ DIF = FormatSpec( "dif/", DeferLoad( diff --git a/acapy_agent/protocols/present_proof/v2_0/routes.py b/acapy_agent/protocols/present_proof/v2_0/routes.py index 2edb0e4c08..b53188f118 100644 --- a/acapy_agent/protocols/present_proof/v2_0/routes.py +++ b/acapy_agent/protocols/present_proof/v2_0/routes.py @@ -16,6 +16,7 @@ from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....anoncreds.holder import AnonCredsHolder, AnonCredsHolderError +from ....anoncreds.models.presentation_request import AnoncredsPresentationRequestSchema from ....connections.models.conn_record import ConnRecord from ....indy.holder import IndyHolder, IndyHolderError from ....indy.models.cred_precis import IndyCredPrecisSchema @@ -118,6 +119,11 @@ class V20PresExRecordListSchema(OpenAPISchema): class V20PresProposalByFormatSchema(OpenAPISchema): """Schema for presentation proposal per format.""" + anoncreds = fields.Nested( + AnoncredsPresentationRequestSchema, + required=False, + metadata={"description": "Presentation proposal for anoncreds"}, + ) indy = fields.Nested( IndyProofRequestSchema, required=False, @@ -192,6 +198,11 @@ class V20PresProposalRequestSchema(AdminAPIMessageTracingSchema): class V20PresRequestByFormatSchema(OpenAPISchema): """Presentation request per format.""" + anoncreds = fields.Nested( + AnoncredsPresentationRequestSchema, + required=False, + metadata={"description": "Presentation proposal for anoncreds"}, + ) indy = fields.Nested( IndyProofRequestSchema, required=False, @@ -293,6 +304,11 @@ class V20PresentationSendRequestToProposalSchema(AdminAPIMessageTracingSchema): class V20PresSpecByFormatRequestSchema(AdminAPIMessageTracingSchema): """Presentation specification schema by format, for send-presentation request.""" + anoncreds = fields.Nested( + IndyPresSpecSchema, + required=False, + metadata={"description": "Presentation specification for anoncreds"}, + ) indy = fields.Nested( IndyPresSpecSchema, required=False, @@ -406,7 +422,10 @@ def _formats_attach(by_format: Mapping, msg_type: str, spec: str) -> Mapping: """Break out formats and proposals/requests/presentations for v2.0 messages.""" attach = [] for fmt_api, item_by_fmt in by_format.items(): - if fmt_api == V20PresFormat.Format.INDY.api: + if ( + fmt_api == V20PresFormat.Format.ANONCREDS.api + or fmt_api == V20PresFormat.Format.INDY.api + ): attach.append(AttachDecorator.data_base64(mapping=item_by_fmt, ident=fmt_api)) elif fmt_api == V20PresFormat.Format.DIF.api: attach.append(AttachDecorator.data_json(mapping=item_by_fmt, ident=fmt_api)) @@ -557,19 +576,24 @@ async def present_proof_credentials_list(request: web.BaseRequest): wallet_type = profile.settings.get_value("wallet.type") if wallet_type == "askar-anoncreds": - indy_holder = AnonCredsHolder(profile) + holder = AnonCredsHolder(profile) else: - indy_holder = profile.inject(IndyHolder) - indy_credentials = [] - # INDY + holder = profile.inject(IndyHolder) + credentials = [] + # ANONCREDS or INDY try: - indy_pres_request = pres_ex_record.by_format["pres_request"].get( - V20PresFormat.Format.INDY.api + # try anoncreds and fallback to indy + pres_request = pres_ex_record.by_format["pres_request"].get( + V20PresFormat.Format.ANONCREDS.api ) - if indy_pres_request: - indy_credentials = ( - await indy_holder.get_credentials_for_presentation_request_by_referent( - indy_pres_request, + if not pres_request: + pres_request = pres_ex_record.by_format["pres_request"].get( + V20PresFormat.Format.INDY.api + ) + if pres_request: + credentials = ( + await holder.get_credentials_for_presentation_request_by_referent( + pres_request, pres_referents, start, count, @@ -771,7 +795,7 @@ async def present_proof_credentials_list(request: web.BaseRequest): pres_ex_record, outbound_handler, ) - credentials = list(indy_credentials) + dif_cred_value_list + credentials = list(credentials) + dif_cred_value_list return web.json_response(credentials) @@ -911,8 +935,11 @@ async def present_proof_create_request(request: web.BaseRequest): comment = body.get("comment") pres_request_spec = body.get("presentation_request") - if pres_request_spec and V20PresFormat.Format.INDY.api in pres_request_spec: - await _add_nonce(pres_request_spec[V20PresFormat.Format.INDY.api]) + if pres_request_spec: + if V20PresFormat.Format.INDY.api in pres_request_spec: + await _add_nonce(pres_request_spec[V20PresFormat.Format.INDY.api]) + if V20PresFormat.Format.ANONCREDS.api in pres_request_spec: + await _add_nonce(pres_request_spec[V20PresFormat.Format.ANONCREDS.api]) pres_request_message = V20PresRequest( comment=comment, @@ -995,8 +1022,11 @@ async def present_proof_send_free_request(request: web.BaseRequest): comment = body.get("comment") pres_request_spec = body.get("presentation_request") - if pres_request_spec and V20PresFormat.Format.INDY.api in pres_request_spec: - await _add_nonce(pres_request_spec[V20PresFormat.Format.INDY.api]) + if pres_request_spec: + if V20PresFormat.Format.INDY.api in pres_request_spec: + await _add_nonce(pres_request_spec[V20PresFormat.Format.INDY.api]) + if V20PresFormat.Format.ANONCREDS.api in pres_request_spec: + await _add_nonce(pres_request_spec[V20PresFormat.Format.ANONCREDS.api]) pres_request_message = V20PresRequest( comment=comment, will_confirm=True, @@ -1157,7 +1187,7 @@ async def present_proof_send_presentation(request: web.BaseRequest): outbound_handler = request["outbound_message_router"] pres_ex_id = request.match_info["pres_ex_id"] body = await request.json() - supported_formats = ["dif", "indy"] + supported_formats = ["anoncreds", "dif", "indy"] if not any(x in body for x in supported_formats): raise web.HTTPBadRequest( reason=( diff --git a/acapy_agent/protocols/present_proof/v2_0/tests/test_manager_anoncreds.py b/acapy_agent/protocols/present_proof/v2_0/tests/test_manager_anoncreds.py index 0ee4f1ad38..78d93e01a7 100644 --- a/acapy_agent/protocols/present_proof/v2_0/tests/test_manager_anoncreds.py +++ b/acapy_agent/protocols/present_proof/v2_0/tests/test_manager_anoncreds.py @@ -74,7 +74,7 @@ ) ], ) -INDY_PROOF_REQ_NAME = { +ANONCREDS_PROOF_REQ_NAME = { "name": PROOF_REQ_NAME, "version": PROOF_REQ_VERSION, "nonce": PROOF_REQ_NONCE, @@ -100,7 +100,7 @@ } }, } -INDY_PROOF_REQ_NAMES = { +ANONCREDS_PROOF_REQ_NAMES = { "name": PROOF_REQ_NAME, "version": PROOF_REQ_VERSION, "nonce": PROOF_REQ_NONCE, @@ -121,7 +121,7 @@ } }, } -INDY_PROOF_REQ_SELFIE = { +ANONCREDS_PROOF_REQ_SELFIE = { "name": PROOF_REQ_NAME, "version": PROOF_REQ_VERSION, "nonce": PROOF_REQ_NONCE, @@ -133,7 +133,7 @@ "0_highscore_GE_uuid": {"name": "highScore", "p_type": ">=", "p_value": 1000000} }, } -INDY_PROOF = { +ANONCREDS_PROOF = { "proof": { "proofs": [ { @@ -252,7 +252,7 @@ } ], } -INDY_PROOF_NAMES = { +ANONCREDS_PROOF_NAMES = { "proof": { "proofs": [ { @@ -512,7 +512,9 @@ async def test_record_eq(self): async def test_create_exchange_for_proposal(self): proposal = V20PresProposal( formats=[ - V20PresFormat(attach_id="indy", format_=V20PresFormat.Format.INDY.aries) + V20PresFormat( + attach_id="anoncreds", format_=V20PresFormat.Format.ANONCREDS.aries + ) ] ) @@ -537,7 +539,9 @@ async def test_receive_proposal(self): connection_record = mock.MagicMock(connection_id=CONN_ID) proposal = V20PresProposal( formats=[ - V20PresFormat(attach_id="indy", format_=V20PresFormat.Format.INDY.aries) + V20PresFormat( + attach_id="anoncreds", format_=V20PresFormat.Format.ANONCREDS.aries + ) ] ) with mock.patch.object(V20PresExRecord, "save", autospec=True) as save_ex: @@ -555,14 +559,14 @@ async def test_create_bound_request_a(self): proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], proposals_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) px_rec = V20PresExRecord( @@ -589,14 +593,14 @@ async def test_create_bound_request_b(self): proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], proposals_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) px_rec = V20PresExRecord( @@ -725,14 +729,16 @@ async def test_create_exchange_for_request(self): will_confirm=True, formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(mapping=INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64( + mapping=ANONCREDS_PROOF_REQ_NAME, ident="anoncreds" + ) ], ) pres_req.assign_thread_id("dummy") @@ -765,14 +771,14 @@ async def test_create_pres_indy(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) @@ -795,9 +801,9 @@ async def test_create_pres_indy(self): ) req_creds = await indy_proof_req_preview2indy_requested_creds( - INDY_PROOF_REQ_NAME, preview=None, holder=self.holder + ANONCREDS_PROOF_REQ_NAME, preview=None, holder=self.holder ) - request_data = {"indy": req_creds} + request_data = {"anoncreds": req_creds} assert not req_creds["self_attested_attributes"] assert len(req_creds["requested_attributes"]) == 2 assert len(req_creds["requested_predicates"]) == 1 @@ -813,9 +819,9 @@ async def test_create_pres_indy_and_dif(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ), V20PresFormat( @@ -826,7 +832,7 @@ async def test_create_pres_indy_and_dif(self): ), ], request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy"), + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds"), AttachDecorator.data_json(DIF_PRES_REQ, ident="dif"), ], ) @@ -857,9 +863,9 @@ async def test_create_pres_indy_and_dif(self): ) req_creds = await indy_proof_req_preview2indy_requested_creds( - INDY_PROOF_REQ_NAME, preview=None, holder=self.holder + ANONCREDS_PROOF_REQ_NAME, preview=None, holder=self.holder ) - request_data = {"indy": req_creds, "dif": DIF_PRES_REQ} + request_data = {"anoncreds": req_creds, "dif": DIF_PRES_REQ} assert not req_creds["self_attested_attributes"] assert len(req_creds["requested_attributes"]) == 2 assert len(req_creds["requested_predicates"]) == 1 @@ -872,19 +878,19 @@ async def test_create_pres_indy_and_dif(self): @pytest.mark.skip(reason="Anoncreds-break") async def test_create_pres_proof_req_non_revoc_interval_none(self): - indy_proof_req_vcx = deepcopy(INDY_PROOF_REQ_NAME) + indy_proof_req_vcx = deepcopy(ANONCREDS_PROOF_REQ_NAME) indy_proof_req_vcx["non_revoked"] = None # simulate interop with indy-vcx pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req_vcx, ident="indy") + AttachDecorator.data_base64(indy_proof_req_vcx, ident="anoncreds") ], ) px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) @@ -918,7 +924,7 @@ async def test_create_pres_proof_req_non_revoc_interval_none(self): req_creds = await indy_proof_req_preview2indy_requested_creds( indy_proof_req_vcx, preview=None, holder=self.holder ) - request_data = {"indy": req_creds} + request_data = {"anoncreds": req_creds} assert not req_creds["self_attested_attributes"] assert len(req_creds["requested_attributes"]) == 2 assert len(req_creds["requested_predicates"]) == 1 @@ -934,14 +940,14 @@ async def test_create_pres_self_asserted(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_SELFIE, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_SELFIE, ident="anoncreds") ], ) px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) @@ -965,9 +971,9 @@ async def test_create_pres_self_asserted(self): ) req_creds = await indy_proof_req_preview2indy_requested_creds( - INDY_PROOF_REQ_SELFIE, preview=None, holder=self.holder + ANONCREDS_PROOF_REQ_SELFIE, preview=None, holder=self.holder ) - request_data = {"indy": req_creds} + request_data = {"anoncreds": req_creds} assert len(req_creds["self_attested_attributes"]) == 3 assert not req_creds["requested_attributes"] @@ -991,14 +997,14 @@ async def test_create_pres_no_revocation(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) @@ -1042,10 +1048,10 @@ async def test_create_pres_no_revocation(self): ) req_creds = await indy_proof_req_preview2indy_requested_creds( - INDY_PROOF_REQ_NAME, preview=None, holder=self.holder + ANONCREDS_PROOF_REQ_NAME, preview=None, holder=self.holder ) request_data = { - "indy": { + "anoncreds": { "self_attested_attributes": req_creds["self_attested_attributes"], "requested_attributes": req_creds["requested_attributes"], "requested_predicates": req_creds["requested_predicates"], @@ -1062,7 +1068,7 @@ async def test_create_pres_no_revocation(self): for pred_reft_spec in req_creds["requested_predicates"].values(): pred_reft_spec["timestamp"] = 1234567890 request_data = { - "indy": { + "anoncreds": { "self_attested_attributes": req_creds["self_attested_attributes"], "requested_attributes": req_creds["requested_attributes"], "requested_predicates": req_creds["requested_predicates"], @@ -1076,14 +1082,14 @@ async def test_create_pres_bad_revoc_state(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) @@ -1147,14 +1153,14 @@ async def test_create_pres_multi_matching_proposal_creds_names(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAMES, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAMES, ident="anoncreds") ], ) px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) @@ -1228,12 +1234,12 @@ async def test_create_pres_multi_matching_proposal_creds_names(self): ) req_creds = await indy_proof_req_preview2indy_requested_creds( - INDY_PROOF_REQ_NAMES, preview=None, holder=self.holder + ANONCREDS_PROOF_REQ_NAMES, preview=None, holder=self.holder ) assert not req_creds["self_attested_attributes"] assert len(req_creds["requested_attributes"]) == 1 assert len(req_creds["requested_predicates"]) == 1 - request_data = {"indy": req_creds} + request_data = {"anoncreds": req_creds} (px_rec_out, pres_msg) = await self.manager.create_pres( px_rec_in, request_data ) @@ -1244,14 +1250,14 @@ async def test_no_matching_creds_for_proof_req(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAMES, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAMES, ident="anoncreds") ], ) V20PresExRecord(pres_request=pres_request.serialize()) @@ -1260,7 +1266,7 @@ async def test_no_matching_creds_for_proof_req(self): with self.assertRaises(ValueError): await indy_proof_req_preview2indy_requested_creds( - INDY_PROOF_REQ_NAMES, preview=None, holder=self.holder + ANONCREDS_PROOF_REQ_NAMES, preview=None, holder=self.holder ) get_creds = mock.CoroutineMock( @@ -1277,21 +1283,21 @@ async def test_no_matching_creds_for_proof_req(self): ) self.holder.get_credentials_for_presentation_request_by_referent = get_creds await indy_proof_req_preview2indy_requested_creds( - INDY_PROOF_REQ_NAMES, preview=None, holder=self.holder + ANONCREDS_PROOF_REQ_NAMES, preview=None, holder=self.holder ) async def test_no_matching_creds_indy_handler(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAMES, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAMES, ident="anoncreds") ], ) px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) @@ -1311,44 +1317,50 @@ async def test_no_matching_creds_indy_handler(self): (px_rec_out, pres_msg) = await self.manager.create_pres( px_rec_in, request_data ) - assert "No matching Indy" in str(context.exception) + assert "AnonCreds interface requires AskarAnoncreds profile" in str( + context.exception + ) async def test_receive_pres(self): connection_record = mock.MagicMock(connection_id=CONN_ID) pres_proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], proposals_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) pres = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(INDY_PROOF, ident="indy")], + presentations_attach=[ + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds") + ], ) pres.assign_thread_id("thread-id") @@ -1360,8 +1372,8 @@ async def test_receive_pres(self): # cover by_format property by_format = px_rec_dummy.by_format - assert by_format.get("pres_proposal").get("indy") == INDY_PROOF_REQ_NAME - assert by_format.get("pres_request").get("indy") == INDY_PROOF_REQ_NAME + assert by_format.get("pres_proposal").get("anoncreds") == ANONCREDS_PROOF_REQ_NAME + assert by_format.get("pres_request").get("anoncreds") == ANONCREDS_PROOF_REQ_NAME with mock.patch.object( V20PresExRecord, "save", autospec=True @@ -1383,41 +1395,45 @@ async def test_receive_pres_receive_pred_value_mismatch_punt_to_indy(self): pres_proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], proposals_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) - indy_proof_req = deepcopy(INDY_PROOF_REQ_NAME) + indy_proof_req = deepcopy(ANONCREDS_PROOF_REQ_NAME) indy_proof_req["requested_predicates"]["0_highscore_GE_uuid"]["restrictions"][0][ "attr::player::value" ] = "impostor" pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(INDY_PROOF, ident="indy")], + presentations_attach=[ + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds") + ], ) pres.assign_thread_id("thread-id") @@ -1429,8 +1445,8 @@ async def test_receive_pres_receive_pred_value_mismatch_punt_to_indy(self): # cover by_format property by_format = px_rec_dummy.by_format - assert by_format.get("pres_proposal").get("indy") == INDY_PROOF_REQ_NAME - assert by_format.get("pres_request").get("indy") == indy_proof_req + assert by_format.get("pres_proposal").get("anoncreds") == ANONCREDS_PROOF_REQ_NAME + assert by_format.get("pres_request").get("anoncreds") == indy_proof_req with mock.patch.object( V20PresExRecord, "save", autospec=True @@ -1478,24 +1494,28 @@ async def test_receive_pres_indy_no_predicate_restrictions(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(INDY_PROOF, ident="indy")], + presentations_attach=[ + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds") + ], ) pres.assign_thread_id("thread-id") @@ -1506,7 +1526,7 @@ async def test_receive_pres_indy_no_predicate_restrictions(self): # cover by_format property by_format = px_rec_dummy.by_format - assert by_format.get("pres_request").get("indy") == indy_proof_req + assert by_format.get("pres_request").get("anoncreds") == indy_proof_req with mock.patch.object( V20PresExRecord, "save", autospec=True @@ -1538,7 +1558,7 @@ async def test_receive_pres_indy_no_attr_restrictions(self): }, "requested_predicates": {}, } - proof = deepcopy(INDY_PROOF) + proof = deepcopy(ANONCREDS_PROOF) proof["requested_proof"]["revealed_attrs"] = { "0_player_uuid": { "sub_proof_index": 0, @@ -1550,24 +1570,26 @@ async def test_receive_pres_indy_no_attr_restrictions(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(proof, ident="indy")], + presentations_attach=[AttachDecorator.data_base64(proof, ident="anoncreds")], ) pres.assign_thread_id("thread-id") @@ -1578,7 +1600,7 @@ async def test_receive_pres_indy_no_attr_restrictions(self): # cover by_format property by_format = px_rec_dummy.by_format - assert by_format.get("pres_request").get("indy") == indy_proof_req + assert by_format.get("pres_request").get("anoncreds") == indy_proof_req with mock.patch.object( V20PresExRecord, "save", autospec=True @@ -1597,44 +1619,48 @@ async def test_receive_pres_indy_no_attr_restrictions(self): async def test_receive_pres_bait_and_switch_attr_name(self): connection_record = mock.MagicMock(connection_id=CONN_ID) - indy_proof_req = deepcopy(INDY_PROOF_REQ_NAME) + indy_proof_req = deepcopy(ANONCREDS_PROOF_REQ_NAME) indy_proof_req["requested_attributes"]["0_screencapture_uuid"]["restrictions"][0][ "attr::screenCapture::value" ] = "c2NyZWVuIGNhcHR1cmUgc2hvd2luZyBzY29yZSBpbiB0aGUgbWlsbGlvbnM=" pres_proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], proposals_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres_x = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(INDY_PROOF, ident="indy")], + presentations_attach=[ + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds") + ], ) px_rec_dummy = V20PresExRecord( pres_proposal=pres_proposal.serialize(), @@ -1655,33 +1681,41 @@ async def test_receive_pres_bait_and_switch_attr_name(self): pres_proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], - proposals_attach=[AttachDecorator.data_base64(indy_proof_req, ident="indy")], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") + ], ) pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres_x = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(INDY_PROOF, ident="indy")], + presentations_attach=[ + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds") + ], ) px_rec_dummy = V20PresExRecord( @@ -1699,43 +1733,47 @@ async def test_receive_pres_bait_and_switch_attr_name(self): async def test_receive_pres_bait_and_switch_attr_names(self): connection_record = mock.MagicMock(connection_id=CONN_ID) - indy_proof_req = deepcopy(INDY_PROOF_REQ_NAMES) + indy_proof_req = deepcopy(ANONCREDS_PROOF_REQ_NAMES) indy_proof_req["requested_attributes"]["0_player_uuid"]["restrictions"][0][ "attr::screenCapture::value" ] = "c2NyZWVuIGNhcHR1cmUgc2hvd2luZyBzY29yZSBpbiB0aGUgbWlsbGlvbnM=" pres_proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], - proposals_attach=[AttachDecorator.data_base64(indy_proof_req, ident="indy")], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") + ], ) pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres_x = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_NAMES, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_NAMES, ident="anoncreds") ], ) @@ -1760,34 +1798,40 @@ async def test_receive_pres_bait_and_switch_attr_names(self): pres_proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], - proposals_attach=[AttachDecorator.data_base64(indy_proof_req, ident="indy")], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") + ], ) pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres_x = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_NAMES, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_NAMES, ident="anoncreds") ], ) @@ -1806,42 +1850,46 @@ async def test_receive_pres_bait_and_switch_attr_names(self): async def test_receive_pres_bait_and_switch_pred(self): connection_record = mock.MagicMock(connection_id=CONN_ID) - indy_proof_req = deepcopy(INDY_PROOF_REQ_NAME) + indy_proof_req = deepcopy(ANONCREDS_PROOF_REQ_NAME) indy_proof_req["requested_predicates"] = {} pres_proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], proposals_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres_x = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(INDY_PROOF, ident="indy")], + presentations_attach=[ + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds") + ], ) px_rec_dummy = V20PresExRecord( @@ -1867,33 +1915,41 @@ async def test_receive_pres_bait_and_switch_pred(self): pres_proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], - proposals_attach=[AttachDecorator.data_base64(indy_proof_req, ident="indy")], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") + ], ) pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres_x = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(INDY_PROOF, ident="indy")], + presentations_attach=[ + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds") + ], ) px_rec_dummy = V20PresExRecord( @@ -1919,33 +1975,41 @@ async def test_receive_pres_bait_and_switch_pred(self): pres_proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], - proposals_attach=[AttachDecorator.data_base64(indy_proof_req, ident="indy")], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") + ], ) pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres_x = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(INDY_PROOF, ident="indy")], + presentations_attach=[ + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds") + ], ) px_rec_dummy = V20PresExRecord( @@ -1971,33 +2035,41 @@ async def test_receive_pres_bait_and_switch_pred(self): pres_proposal = V20PresProposal( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], - proposals_attach=[AttachDecorator.data_base64(indy_proof_req, ident="indy")], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") + ], ) pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], request_presentations_attach=[ - AttachDecorator.data_base64(indy_proof_req, ident="indy") + AttachDecorator.data_base64(indy_proof_req, ident="anoncreds") ], ) pres_x = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(INDY_PROOF, ident="indy")], + presentations_attach=[ + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds") + ], ) px_rec_dummy = V20PresExRecord( @@ -2020,25 +2092,29 @@ async def test_verify_pres(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ) ], will_confirm=True, request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds") ], ) pres = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ) ], - presentations_attach=[AttachDecorator.data_base64(INDY_PROOF, ident="indy")], + presentations_attach=[ + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds") + ], ) px_rec_in = V20PresExRecord( pres_request=pres_request, @@ -2063,9 +2139,9 @@ async def test_verify_pres_indy_and_dif(self): pres_request = V20PresRequest( formats=[ V20PresFormat( - attach_id="indy", + attach_id="anoncreds", format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ - V20PresFormat.Format.INDY.api + V20PresFormat.Format.ANONCREDS.api ], ), V20PresFormat( @@ -2077,15 +2153,17 @@ async def test_verify_pres_indy_and_dif(self): ], will_confirm=True, request_presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy"), + AttachDecorator.data_base64(ANONCREDS_PROOF_REQ_NAME, ident="anoncreds"), AttachDecorator.data_json(DIF_PRES_REQ, ident="dif"), ], ) pres = V20Pres( formats=[ V20PresFormat( - attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + attach_id="anoncreds", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.ANONCREDS.api + ], ), V20PresFormat( attach_id="dif", @@ -2093,7 +2171,7 @@ async def test_verify_pres_indy_and_dif(self): ), ], presentations_attach=[ - AttachDecorator.data_base64(INDY_PROOF, ident="indy"), + AttachDecorator.data_base64(ANONCREDS_PROOF, ident="anoncreds"), AttachDecorator.data_json(DIF_PRES, ident="dif"), ], ) diff --git a/acapy_agent/protocols/present_proof/v2_0/tests/test_routes.py b/acapy_agent/protocols/present_proof/v2_0/tests/test_routes.py index af70cd2d0b..b1fb15ef7f 100644 --- a/acapy_agent/protocols/present_proof/v2_0/tests/test_routes.py +++ b/acapy_agent/protocols/present_proof/v2_0/tests/test_routes.py @@ -6,8 +6,10 @@ from marshmallow import ValidationError from .....admin.request_context import AdminRequestContext +from .....anoncreds.models.presentation_request import ( + AnoncredsPresentationReqAttrSpecSchema, +) from .....indy.holder import IndyHolder -from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....indy.verifier import IndyVerifier from .....ledger.base import BaseLedger from .....storage.error import StorageNotFoundError @@ -221,7 +223,7 @@ async def test_validate(self): schema.validate_fields({"veres-one": {"no": "support"}}) async def test_validate_proof_req_attr_spec(self): - aspec = IndyProofReqAttrSpecSchema() + aspec = AnoncredsPresentationReqAttrSpecSchema() aspec.validate_fields({"name": "attr0"}) aspec.validate_fields( { diff --git a/acapy_agent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py b/acapy_agent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py index 96c3da744e..faa4f0f278 100644 --- a/acapy_agent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py +++ b/acapy_agent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py @@ -8,8 +8,10 @@ from .....admin.request_context import AdminRequestContext from .....anoncreds.holder import AnonCredsHolder +from .....anoncreds.models.presentation_request import ( + AnoncredsPresentationReqAttrSpecSchema, +) from .....anoncreds.verifier import AnonCredsVerifier -from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....ledger.base import BaseLedger from .....storage.error import StorageNotFoundError from .....storage.vc_holder.base import VCHolder @@ -223,7 +225,7 @@ async def test_validate(self): schema.validate_fields({"veres-one": {"no": "support"}}) async def test_validate_proof_req_attr_spec(self): - aspec = IndyProofReqAttrSpecSchema() + aspec = AnoncredsPresentationReqAttrSpecSchema() aspec.validate_fields({"name": "attr0"}) aspec.validate_fields( { diff --git a/acapy_agent/revocation_anoncreds/routes.py b/acapy_agent/revocation_anoncreds/routes.py index 928ef0ebd8..4c5b3bd8b0 100644 --- a/acapy_agent/revocation_anoncreds/routes.py +++ b/acapy_agent/revocation_anoncreds/routes.py @@ -24,7 +24,7 @@ ) from ..anoncreds.default.legacy_indy.registry import LegacyIndyRegistry from ..anoncreds.issuer import AnonCredsIssuerError -from ..anoncreds.models.anoncreds_revocation import RevRegDefState +from ..anoncreds.models.revocation import RevRegDefState from ..anoncreds.revocation import AnonCredsRevocation, AnonCredsRevocationError from ..anoncreds.routes import ( create_transaction_for_endorser_description, diff --git a/acapy_agent/revocation_anoncreds/tests/test_routes.py b/acapy_agent/revocation_anoncreds/tests/test_routes.py index 11d4ea25e3..4fef93761e 100644 --- a/acapy_agent/revocation_anoncreds/tests/test_routes.py +++ b/acapy_agent/revocation_anoncreds/tests/test_routes.py @@ -6,7 +6,7 @@ from aiohttp.web import HTTPNotFound from ...admin.request_context import AdminRequestContext -from ...anoncreds.models.anoncreds_revocation import RevRegDef, RevRegDefValue +from ...anoncreds.models.revocation import RevRegDef, RevRegDefValue from ...tests import mock from ...utils.testing import create_test_profile from .. import routes as test_module diff --git a/acapy_agent/vc/vc_ld/models/linked_data_proof.py b/acapy_agent/vc/vc_ld/models/linked_data_proof.py index c441ff5911..57853000c6 100644 --- a/acapy_agent/vc/vc_ld/models/linked_data_proof.py +++ b/acapy_agent/vc/vc_ld/models/linked_data_proof.py @@ -6,8 +6,8 @@ from ....messaging.models.base import BaseModel, BaseModelSchema from ....messaging.valid import ( - INDY_ISO8601_DATETIME_EXAMPLE, - INDY_ISO8601_DATETIME_VALIDATE, + ISO8601_DATETIME_EXAMPLE, + ISO8601_DATETIME_VALIDATE, UUID4_EXAMPLE, Uri, ) @@ -93,13 +93,13 @@ class Meta: created = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": ( "The string value of an ISO8601 combined date and time string generated" " by the Signature Algorithm" ), - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) diff --git a/acapy_agent/vc/vc_ld/models/options.py b/acapy_agent/vc/vc_ld/models/options.py index 2d7adbe4e2..a939df56ac 100644 --- a/acapy_agent/vc/vc_ld/models/options.py +++ b/acapy_agent/vc/vc_ld/models/options.py @@ -5,8 +5,8 @@ from marshmallow import INCLUDE, Schema, fields from acapy_agent.messaging.valid import ( - INDY_ISO8601_DATETIME_EXAMPLE, - INDY_ISO8601_DATETIME_VALIDATE, + ISO8601_DATETIME_EXAMPLE, + ISO8601_DATETIME_VALIDATE, UUID4_EXAMPLE, ) @@ -124,13 +124,13 @@ class Meta: created = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, + validate=ISO8601_DATETIME_VALIDATE, metadata={ "description": ( "The date and time of the proof (with a maximum accuracy in seconds)." " Defaults to current system time" ), - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": ISO8601_DATETIME_EXAMPLE, }, ) diff --git a/acapy_agent/wallet/anoncreds_upgrade.py b/acapy_agent/wallet/anoncreds_upgrade.py index 12f2cb2dd3..ce944c75d7 100644 --- a/acapy_agent/wallet/anoncreds_upgrade.py +++ b/acapy_agent/wallet/anoncreds_upgrade.py @@ -21,15 +21,15 @@ CATEGORY_CRED_DEF_PRIVATE, CATEGORY_SCHEMA, ) -from ..anoncreds.models.anoncreds_cred_def import CredDef, CredDefState -from ..anoncreds.models.anoncreds_revocation import ( +from ..anoncreds.models.credential_definition import CredDef, CredDefState +from ..anoncreds.models.revocation import ( RevList, RevListState, RevRegDef, RevRegDefState, RevRegDefValue, ) -from ..anoncreds.models.anoncreds_schema import SchemaState +from ..anoncreds.models.schema import SchemaState from ..anoncreds.revocation import ( CATEGORY_REV_LIST, CATEGORY_REV_REG_DEF, diff --git a/acapy_agent/wallet/routes.py b/acapy_agent/wallet/routes.py index 857e783814..45b96edcb4 100644 --- a/acapy_agent/wallet/routes.py +++ b/acapy_agent/wallet/routes.py @@ -9,11 +9,10 @@ from aiohttp_apispec import docs, querystring_schema, request_schema, response_schema from marshmallow import fields, validate -from acapy_agent.connections.base_manager import BaseConnectionManager - from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..config.injection_context import InjectionContext +from ..connections.base_manager import BaseConnectionManager from ..connections.models.conn_record import ConnRecord from ..core.event_bus import Event, EventBus from ..core.profile import Profile @@ -35,12 +34,12 @@ GENERIC_DID_VALIDATE, INDY_DID_EXAMPLE, INDY_DID_VALIDATE, - INDY_RAW_PUBLIC_KEY_EXAMPLE, - INDY_RAW_PUBLIC_KEY_VALIDATE, JWT_EXAMPLE, JWT_VALIDATE, NON_SD_LIST_EXAMPLE, NON_SD_LIST_VALIDATE, + RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, + RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, SD_JWT_EXAMPLE, SD_JWT_VALIDATE, UUID4_EXAMPLE, @@ -104,10 +103,10 @@ class DIDSchema(OpenAPISchema): ) verkey = fields.Str( required=True, - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Public verification key", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ) posture = fields.Str( @@ -302,10 +301,10 @@ class DIDListQueryStringSchema(OpenAPISchema): ) verkey = fields.Str( required=False, - validate=INDY_RAW_PUBLIC_KEY_VALIDATE, + validate=RAW_ED25519_2018_PUBLIC_KEY_VALIDATE, metadata={ "description": "Verification key of interest", - "example": INDY_RAW_PUBLIC_KEY_EXAMPLE, + "example": RAW_ED25519_2018_PUBLIC_KEY_EXAMPLE, }, ) posture = fields.Str( diff --git a/demo/bdd_support/agent_backchannel_client.py b/demo/bdd_support/agent_backchannel_client.py index 5d116e2fea..fa6db011c7 100644 --- a/demo/bdd_support/agent_backchannel_client.py +++ b/demo/bdd_support/agent_backchannel_client.py @@ -143,11 +143,13 @@ def aries_container_issue_credential( the_container: AgentContainer, cred_def_id: str, cred_attrs: list, + filter_type: str = "indy", ): return run_coroutine( the_container.issue_credential, cred_def_id, cred_attrs, + filter_type=filter_type, ) @@ -167,11 +169,13 @@ def aries_container_request_proof( the_container: AgentContainer, proof_request: dict, explicit_revoc_required: bool = False, + is_anoncreds: bool = False, ): return run_coroutine( the_container.request_proof, proof_request, explicit_revoc_required=explicit_revoc_required, + is_anoncreds=is_anoncreds, ) diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index 97106d3e1d..d96fdae10a 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -62,10 +62,9 @@ def step_impl(context, issuer, credential_data): agent = context.active_agents[issuer] cred_attrs = read_credential_data(context.schema_name, credential_data) + filter_type = "indy" if not is_anoncreds(agent) else "anoncreds" cred_exchange = aries_container_issue_credential( - agent["agent"], - context.cred_def_id, - cred_attrs, + agent["agent"], context.cred_def_id, cred_attrs, filter_type ) context.cred_attrs = cred_attrs diff --git a/demo/features/steps/0454-present-proof.py b/demo/features/steps/0454-present-proof.py index 984ee72e63..31e91930f5 100644 --- a/demo/features/steps/0454-present-proof.py +++ b/demo/features/steps/0454-present-proof.py @@ -34,7 +34,11 @@ def step_impl(context, verifier, request_for_proof, prover): "restrictions" ] = cred_def_restrictions - proof_exchange = aries_container_request_proof(agent["agent"], proof_request_info) + proof_exchange = aries_container_request_proof( + agent["agent"], + proof_request_info, + agent["agent"].wallet_type == "askar-anoncreds", + ) context.proof_request = proof_request_info context.proof_exchange = proof_exchange @@ -49,7 +53,10 @@ def step_impl(context, verifier, request_for_proof, prover): proof_request_info = read_proof_req_data(request_for_proof) proof_exchange = aries_container_request_proof( - agent["agent"], proof_request_info, explicit_revoc_required=True + agent["agent"], + proof_request_info, + explicit_revoc_required=True, + is_anoncreds=agent["agent"].wallet_type == "askar-anoncreds", ) context.proof_request = proof_request_info diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index bf5a8a0c3c..f8ade72f98 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -593,14 +593,15 @@ def step_impl(context, agent_name): agent["agent"], "/issue-credential-2.0/records/" + cred_exchange["cred_ex_id"] ) context.cred_exchange = cred_exchange + cred_exchange_format = cred_exchange.get("indy") or cred_exchange.get("anoncreds") agent_container_POST( agent["agent"], endpoint, data={ - "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], + "cred_rev_id": cred_exchange_format["cred_rev_id"], "publish": False, - "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], + "rev_reg_id": cred_exchange_format["rev_reg_id"], "connection_id": cred_exchange["cred_ex_record"]["connection_id"], }, ) @@ -627,11 +628,13 @@ def step_impl(context, agent_name): context.cred_exchange = cred_exchange connection_id = agent["agent"].agent.connection_id + cred_exchange_format = cred_exchange.get("indy") or cred_exchange.get("anoncreds") + # revoke the credential if not is_anoncreds(agent): data = { - "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], - "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], + "rev_reg_id": cred_exchange_format["rev_reg_id"], + "cred_rev_id": cred_exchange_format["cred_rev_id"], "publish": False, "connection_id": cred_exchange["cred_ex_record"]["connection_id"], } @@ -642,9 +645,9 @@ def step_impl(context, agent_name): endpoint = "/revocation/revoke" else: data = { - "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], + "cred_rev_id": cred_exchange_format["cred_rev_id"], "publish": False, - "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], + "rev_reg_id": cred_exchange_format["rev_reg_id"], "connection_id": cred_exchange["cred_ex_record"]["connection_id"], "options": { "endorser_connection_id": connection_id, @@ -676,15 +679,17 @@ def step_impl(context, agent_name): else: endpoint = "/anoncreds/revocation/publish-revocations" + cred_exchange_format = context.cred_exchange.get("indy") or context.cred_exchange.get( + "anoncreds" + ) + # create rev_reg entry transaction created_rev_reg = agent_container_POST( agent["agent"], endpoint, data={ "rrid2crid": { - context.cred_exchange["indy"]["rev_reg_id"]: [ - context.cred_exchange["indy"]["cred_rev_id"] - ] + cred_exchange_format["rev_reg_id"]: [cred_exchange_format["cred_rev_id"]] } }, params={}, @@ -703,14 +708,15 @@ def step_impl(context, agent_name): agent = context.active_agents[agent_name] connection_id = agent["agent"].agent.connection_id + cred_exchange_format = context.cred_exchange.get("indy") or context.cred_exchange.get( + "anoncreds" + ) # create rev_reg entry transaction if not is_anoncreds(agent): data = { "rrid2crid": { - context.cred_exchange["indy"]["rev_reg_id"]: [ - context.cred_exchange["indy"]["cred_rev_id"] - ] + cred_exchange_format["rev_reg_id"]: [cred_exchange_format["cred_rev_id"]] } } params = { @@ -721,9 +727,7 @@ def step_impl(context, agent_name): else: data = { "rrid2crid": { - context.cred_exchange["indy"]["rev_reg_id"]: [ - context.cred_exchange["indy"]["cred_rev_id"] - ] + cred_exchange_format["rev_reg_id"]: [cred_exchange_format["cred_rev_id"]] }, "options": { "endorser_connection_id": connection_id, diff --git a/demo/features/upgrade.feature b/demo/features/upgrade.feature index 259c7a485a..557dd75646 100644 --- a/demo/features/upgrade.feature +++ b/demo/features/upgrade.feature @@ -19,10 +19,7 @@ Feature: ACA-Py Anoncreds Upgrade Then "Faber" has the proof verification fail Then "Bob" can verify the credential from "" was revoked And "" upgrades the wallet to anoncreds - And "Bob" has an issued credential from "" And "Bob" upgrades the wallet to anoncreds - And "Bob" has an issued credential from "" - When "Faber" sends a request for proof presentation to "Bob" Examples: | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index c2ad39fe5f..61b07f4ef7 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -36,6 +36,8 @@ log_timer, ) +from .support.agent import CRED_FORMAT_ANONCREDS + CRED_PREVIEW_TYPE = "https://didcomm.org/issue-credential/2.0/credential-preview" SELF_ATTESTED = os.getenv("SELF_ATTESTED") TAILS_FILE_COUNT = int(os.getenv("TAILS_FILE_COUNT", 100)) @@ -272,16 +274,25 @@ async def handle_issue_credential_v2_0(self, message): elif state == "offer-received": log_status("#15 After receiving credential offer, send credential request") + + def _should_send_request_without_data(message): + """Formats that do not require credential request data.""" + cred_offer_by_format = message["by_format"].get("cred_offer") + + return ( + not message.get("by_format") + or cred_offer_by_format.get("anoncreds") + or cred_offer_by_format.get("indy") + or cred_offer_by_format.get("vc_di") + ) + # Should wait for a tiny bit for the delete tests await asyncio.sleep(0.2) if not message.get("by_format"): # this should not happen, something hinky when running in IDE... # this will work if using indy payloads self.log(f"No 'by_format' in message: {message}") - await self.admin_POST( - f"/issue-credential-2.0/records/{cred_ex_id}/send-request" - ) - elif message["by_format"]["cred_offer"].get("indy"): + elif _should_send_request_without_data(message): await self.admin_POST( f"/issue-credential-2.0/records/{cred_ex_id}/send-request" ) @@ -294,10 +305,6 @@ async def handle_issue_credential_v2_0(self, message): await self.admin_POST( f"/issue-credential-2.0/records/{cred_ex_id}/send-request", data ) - elif message["by_format"]["cred_offer"].get("vc_di"): - await self.admin_POST( - f"/issue-credential-2.0/records/{cred_ex_id}/send-request" - ) elif state == "done": pass @@ -327,6 +334,26 @@ async def handle_issue_credential_v2_0_indy(self, message): self.log(f"Revocation registry ID: {rev_reg_id}") self.log(f"Credential revocation ID: {cred_rev_id}") + async def handle_issue_credential_v2_0_anoncreds(self, message): + rev_reg_id = message.get("rev_reg_id") + cred_rev_id = message.get("cred_rev_id") + cred_id_stored = message.get("cred_id_stored") + + if cred_id_stored: + cred_id = message["cred_id_stored"] + log_status(f"#18.1 Stored credential {cred_id} in wallet") + cred = await self.admin_GET(f"/credential/{cred_id}") + log_json(cred, label="Credential details:") + self.log("credential_id", cred_id) + self.log("cred_def_id", cred["cred_def_id"]) + self.log("schema_id", cred["schema_id"]) + # track last successfully received credential + self.last_credential_received = cred + + if rev_reg_id and cred_rev_id: + self.log(f"Revocation registry ID: {rev_reg_id}") + self.log(f"Credential revocation ID: {cred_rev_id}") + async def handle_issue_credential_v2_0_vc_di(self, message): self.log(f"Handle VC_DI Credential: message = {message}") @@ -442,16 +469,18 @@ async def handle_present_proof_v2_0(self, message): # this should not happen, something hinky when running in IDE... self.log(f"No 'by_format' in message: {message}") else: - pres_request_indy = ( - message["by_format"].get("pres_request", {}).get("indy") - ) - pres_request_dif = message["by_format"].get("pres_request", {}).get("dif") + pres_request_by_format = message["by_format"].get("pres_request", {}) + pres_request = pres_request_by_format.get( + "indy" + ) or pres_request_by_format.get("anoncreds") + + pres_request_dif = pres_request_by_format.get("dif") request = {} - if not pres_request_dif and not pres_request_indy: + if not pres_request_dif and not pres_request: raise Exception("Invalid presentation request received") - if pres_request_indy: + if pres_request: # include self-attested attributes (not included in credentials) creds_by_reft = {} revealed = {} @@ -463,7 +492,6 @@ async def handle_present_proof_v2_0(self, message): creds = await self.admin_GET( f"/present-proof-2.0/records/{pres_ex_id}/credentials" ) - # print(">>> creds:", creds) if creds: # select only indy credentials creds = [x for x in creds if "cred_info" in x] @@ -484,7 +512,7 @@ async def handle_present_proof_v2_0(self, message): # submit the proof wit one unrevealed revealed attribute revealed_flag = False - for referent in pres_request_indy["requested_attributes"]: + for referent in pres_request["requested_attributes"]: if referent in creds_by_reft: revealed[referent] = { "cred_id": creds_by_reft[referent]["cred_info"][ @@ -496,7 +524,7 @@ async def handle_present_proof_v2_0(self, message): else: self_attested[referent] = "my self-attested value" - for referent in pres_request_indy["requested_predicates"]: + for referent in pres_request["requested_predicates"]: if referent in creds_by_reft: predicates[referent] = { "cred_id": creds_by_reft[referent]["cred_info"][ @@ -504,15 +532,15 @@ async def handle_present_proof_v2_0(self, message): ] } - log_status("#25 Generate the indy proof") - indy_request = { - "indy": { + log_status("#25 Generate the proof") + request = { + "indy" if "indy" in pres_request_by_format else "anoncreds": { "requested_predicates": predicates, "requested_attributes": revealed, "self_attested_attributes": self_attested, } } - request.update(indy_request) + request.update(request) except ClientError: pass @@ -779,7 +807,7 @@ def __init__( # endorsers and authors need public DIDs (assume cred_type is Indy) if endorser_role == "author" or endorser_role == "endorser": self.public_did = True - self.cred_type = CRED_FORMAT_INDY + # self.cred_type = CRED_FORMAT_INDY self.reuse_connections = reuse_connections self.multi_use_invitations = multi_use_invitations @@ -938,7 +966,7 @@ async def create_schema_and_cred_def( ): if not self.public_did: raise Exception("Can't create a schema/cred def without a public DID :-(") - if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: + if self.cred_type in [CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: # need to redister schema and cred def on the ledger self.cred_def_id = await self.agent.create_schema_and_cred_def( schema_name, @@ -981,20 +1009,26 @@ async def issue_credential( self, cred_def_id: str, cred_attrs: list, + filter_type: str = "indy", ): log_status("#13 Issue credential offer to X") - if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: + if self.cred_type in [CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: cred_preview = { "@type": CRED_PREVIEW_TYPE, "attributes": cred_attrs, } + if filter_type == "indy": + _filter = {"indy": {"cred_def_id": cred_def_id}} + else: + _filter = {"anoncreds": {"cred_def_id": cred_def_id}} + offer_request = { "connection_id": self.agent.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}}, + "filter": _filter, "trace": self.exchange_tracing, } cred_exchange = await self.agent.admin_POST( @@ -1041,11 +1075,13 @@ async def receive_credential( return matched - async def request_proof(self, proof_request, explicit_revoc_required: bool = False): + async def request_proof( + self, proof_request, explicit_revoc_required: bool = False, is_anoncreds=False + ): log_status("#20 Request proof of degree from alice") - if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: - indy_proof_request = { + if self.cred_type in [CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: + proof_request = { "name": ( proof_request["name"] if "name" in proof_request else "Proof of stuff" ), @@ -1061,24 +1097,24 @@ async def request_proof(self, proof_request, explicit_revoc_required: bool = Fal # plug in revocation where requested in the supplied proof request non_revoked = {"to": int(time.time())} if "non_revoked" in proof_request: - indy_proof_request["non_revoked"] = non_revoked + proof_request["non_revoked"] = non_revoked non_revoked_supplied = True for attr in proof_request["requested_attributes"]: if "non_revoked" in proof_request["requested_attributes"][attr]: - indy_proof_request["requested_attributes"][attr][ - "non_revoked" - ] = non_revoked + proof_request["requested_attributes"][attr]["non_revoked"] = ( + non_revoked + ) non_revoked_supplied = True for pred in proof_request["requested_predicates"]: if "non_revoked" in proof_request["requested_predicates"][pred]: - indy_proof_request["requested_predicates"][pred][ - "non_revoked" - ] = non_revoked + proof_request["requested_predicates"][pred]["non_revoked"] = ( + non_revoked + ) non_revoked_supplied = True if not non_revoked_supplied and not explicit_revoc_required: # else just make it global - indy_proof_request["non_revoked"] = non_revoked + proof_request["non_revoked"] = non_revoked else: # make sure we are not leaking non-revoc requests @@ -1091,13 +1127,16 @@ async def request_proof(self, proof_request, explicit_revoc_required: bool = Fal if "non_revoked" in proof_request["requested_predicates"][pred]: del proof_request["requested_predicates"][pred]["non_revoked"] - log_status(f" >>> asking for proof for request: {indy_proof_request}") + log_status(f" >>> asking for proof for request: {proof_request}") + + if is_anoncreds: + presentation_request = {"anoncreds": proof_request} + else: + presentation_request = {"indy": proof_request} proof_request_web_request = { "connection_id": self.agent.connection_id, - "presentation_request": { - "indy": indy_proof_request, - }, + "presentation_request": presentation_request, "trace": self.exchange_tracing, } proof_exchange = await self.agent.admin_POST( @@ -1108,7 +1147,6 @@ async def request_proof(self, proof_request, explicit_revoc_required: bool = Fal elif self.cred_type == CRED_FORMAT_JSON_LD: # TODO create and send the json-ld proof request - pass return None else: @@ -1125,7 +1163,7 @@ async def verify_proof(self, proof_request): # log_status(f">>> last proof received: {self.agent.last_proof_received}") - if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: + if self.cred_type in [CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: # return verified status return self.agent.last_proof_received["verified"] @@ -1514,12 +1552,14 @@ async def create_agent_with_args(args, ident: str = None, extra_args: list = Non aip = 20 if "cred_type" in args and args.cred_type not in [ + CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_VC_DI, ]: public_did = None aip = 20 elif "cred_type" in args and args.cred_type in [ + CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_VC_DI, ]: @@ -1528,6 +1568,12 @@ async def create_agent_with_args(args, ident: str = None, extra_args: list = Non public_did = args.public_did if "public_did" in args else None cred_type = args.cred_type if "cred_type" in args else None + + # Set anoncreds agent to use anoncreds credential format + wallet_type = arg_file_dict.get("wallet-type") or args.wallet_type + if wallet_type == "askar-anoncreds": + cred_type = CRED_FORMAT_ANONCREDS + log_msg( f"Initializing demo agent {agent_ident} with AIP {aip} and credential type {cred_type}" ) @@ -1564,7 +1610,7 @@ async def create_agent_with_args(args, ident: str = None, extra_args: list = Non mediation=args.mediation, cred_type=cred_type, use_did_exchange=(aip == 20) if ("aip" in args) else args.did_exchange, - wallet_type=arg_file_dict.get("wallet-type") or args.wallet_type, + wallet_type=wallet_type, public_did=public_did, seed="random" if public_did else None, arg_file=arg_file, diff --git a/demo/runners/faber.py b/demo/runners/faber.py index 5cecf47d37..00cc9a2e98 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -17,6 +17,7 @@ create_agent_with_args, ) from runners.support.agent import ( # noqa:E402 + CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_JSON_LD, CRED_FORMAT_VC_DI, @@ -111,7 +112,7 @@ def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracin return offer_request elif aip == 20: - if cred_type == CRED_FORMAT_INDY: + if cred_type == CRED_FORMAT_ANONCREDS or cred_type == CRED_FORMAT_INDY: self.cred_attrs[cred_def_id] = { "name": "Alice Smith", "date": "2018-05-28", @@ -127,12 +128,16 @@ def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracin for (n, v) in self.cred_attrs[cred_def_id].items() ], } + if cred_type == CRED_FORMAT_ANONCREDS: + _filter = {"anoncreds": {"cred_def_id": cred_def_id}} + else: + _filter = {"indy": {"cred_def_id": cred_def_id}} offer_request = { "connection_id": self.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}}, + "filter": _filter, "trace": exchange_tracing, } return offer_request @@ -249,7 +254,7 @@ def generate_proof_request_web_request( "restrictions": [{"schema_name": "degree schema"}], } ] - indy_proof_request = { + proof_request = { "name": "Proof of Education", "version": "1.0", "requested_attributes": { @@ -261,10 +266,10 @@ def generate_proof_request_web_request( } if revocation: - indy_proof_request["non_revoked"] = {"to": int(time.time())} + proof_request["non_revoked"] = {"to": int(time.time())} proof_request_web_request = { - "proof_request": indy_proof_request, + "proof_request": proof_request, "trace": exchange_tracing, } if not connectionless: @@ -272,7 +277,7 @@ def generate_proof_request_web_request( return proof_request_web_request elif aip == 20: - if cred_type == CRED_FORMAT_INDY: + if cred_type == CRED_FORMAT_ANONCREDS or cred_type == CRED_FORMAT_INDY: req_attrs = [ { "name": "name", @@ -312,7 +317,7 @@ def generate_proof_request_web_request( "restrictions": [{"schema_name": "degree schema"}], } ] - indy_proof_request = { + proof_request = { "name": "Proof of Education", "version": "1.0", "requested_attributes": { @@ -325,14 +330,19 @@ def generate_proof_request_web_request( } if revocation: - indy_proof_request["non_revoked"] = {"to": int(time.time())} + proof_request["non_revoked"] = {"to": int(time.time())} + if cred_type == CRED_FORMAT_ANONCREDS: + presentation_request = {"anoncreds": proof_request} + else: + presentation_request = {"indy": proof_request} proof_request_web_request = { - "presentation_request": {"indy": indy_proof_request}, + "presentation_request": presentation_request, "trace": exchange_tracing, } if not connectionless: proof_request_web_request["connection_id"] = self.connection_id + return proof_request_web_request elif cred_type == CRED_FORMAT_VC_DI: @@ -537,7 +547,11 @@ async def main(args): "birthdate_dateint", "timestamp", ] - if faber_agent.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: + if faber_agent.cred_type in [ + CRED_FORMAT_ANONCREDS, + CRED_FORMAT_INDY, + CRED_FORMAT_VC_DI, + ]: faber_agent.public_did = True await faber_agent.initialize( the_agent=agent, @@ -569,6 +583,7 @@ async def main(args): exchange_tracing = False options = " (1) Issue Credential\n" if faber_agent.cred_type in [ + CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_VC_DI, ]: @@ -664,12 +679,14 @@ async def main(args): elif option == "1a": new_cred_type = await prompt( - "Enter credential type ({}, {}): ".format( + "Enter credential type ({}, {}, {}): ".format( + CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_VC_DI, ) ) if new_cred_type in [ + CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_VC_DI, ]: @@ -689,7 +706,11 @@ async def main(args): ) elif faber_agent.aip == 20: - if faber_agent.cred_type == CRED_FORMAT_INDY: + if faber_agent.cred_type in [ + CRED_FORMAT_ANONCREDS, + CRED_FORMAT_INDY, + CRED_FORMAT_VC_DI, + ]: offer_request = faber_agent.agent.generate_credential_offer( faber_agent.aip, faber_agent.cred_type, @@ -705,14 +726,6 @@ async def main(args): exchange_tracing, ) - elif faber_agent.cred_type == CRED_FORMAT_VC_DI: - offer_request = faber_agent.agent.generate_credential_offer( - faber_agent.aip, - faber_agent.cred_type, - faber_agent.cred_def_id, - exchange_tracing, - ) - else: raise Exception( f"Error invalid credential type: {faber_agent.cred_type}" @@ -742,27 +755,12 @@ async def main(args): pass elif faber_agent.aip == 20: - if faber_agent.cred_type == CRED_FORMAT_INDY: - proof_request_web_request = ( - faber_agent.agent.generate_proof_request_web_request( - faber_agent.aip, - faber_agent.cred_type, - faber_agent.revocation, - exchange_tracing, - ) - ) - - elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: - proof_request_web_request = ( - faber_agent.agent.generate_proof_request_web_request( - faber_agent.aip, - faber_agent.cred_type, - faber_agent.revocation, - exchange_tracing, - ) - ) - - elif faber_agent.cred_type == CRED_FORMAT_VC_DI: + if faber_agent.cred_type in [ + CRED_FORMAT_ANONCREDS, + CRED_FORMAT_INDY, + CRED_FORMAT_VC_DI, + CRED_FORMAT_JSON_LD, + ]: proof_request_web_request = ( faber_agent.agent.generate_proof_request_web_request( faber_agent.aip, @@ -771,7 +769,6 @@ async def main(args): exchange_tracing, ) ) - else: raise Exception( "Error invalid credential type:" + faber_agent.cred_type @@ -819,28 +816,12 @@ async def main(args): qr.print_ascii(invert=True) elif faber_agent.aip == 20: - if faber_agent.cred_type == CRED_FORMAT_INDY: - proof_request_web_request = ( - faber_agent.agent.generate_proof_request_web_request( - faber_agent.aip, - faber_agent.cred_type, - faber_agent.revocation, - exchange_tracing, - connectionless=True, - ) - ) - elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: - proof_request_web_request = ( - faber_agent.agent.generate_proof_request_web_request( - faber_agent.aip, - faber_agent.cred_type, - faber_agent.revocation, - exchange_tracing, - connectionless=True, - ) - ) - - elif faber_agent.cred_type == CRED_FORMAT_VC_DI: + if faber_agent.cred_type in [ + CRED_FORMAT_ANONCREDS, + CRED_FORMAT_INDY, + CRED_FORMAT_VC_DI, + CRED_FORMAT_JSON_LD, + ]: proof_request_web_request = ( faber_agent.agent.generate_proof_request_web_request( faber_agent.aip, diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 2905ad4912..5d1e10ccd6 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -70,6 +70,7 @@ WALLET_TYPE_ASKAR = "askar" WALLET_TYPE_ANONCREDS = "askar-anoncreds" +CRED_FORMAT_ANONCREDS = "anoncreds" CRED_FORMAT_INDY = "indy" CRED_FORMAT_JSON_LD = "json-ld" CRED_FORMAT_VC_DI = "vc_di" @@ -672,7 +673,7 @@ async def register_did( role: str = "TRUST_ANCHOR", cred_type: str = CRED_FORMAT_INDY, ): - if cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: + if cred_type in [CRED_FORMAT_ANONCREDS, CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: # if registering a did for issuing indy credentials, publish the did on the ledger self.log(f"Registering {self.ident} ...") if not ledger_url: diff --git a/docker/Dockerfile.run b/docker/Dockerfile.run index 7211df51cb..f6a7e2f8ff 100644 --- a/docker/Dockerfile.run +++ b/docker/Dockerfile.run @@ -18,7 +18,7 @@ RUN mkdir -p acapy_agent && touch acapy_agent/__init__.py ADD pyproject.toml poetry.lock README.md ./ RUN mkdir -p log && chmod -R ug+rw log -ARG all_extras=0 +ARG all_extras RUN if ! [ -z ${all_extras} ]; then poetry install --all-extras; else poetry install -E "askar didcommv2"; fi ADD . . diff --git a/scenarios/examples/anoncreds_issuance_and_revocation/docker-compose.yml b/scenarios/examples/anoncreds_issuance_and_revocation/docker-compose.yml index 247a0b7daf..6e0a757fbd 100644 --- a/scenarios/examples/anoncreds_issuance_and_revocation/docker-compose.yml +++ b/scenarios/examples/anoncreds_issuance_and_revocation/docker-compose.yml @@ -1,22 +1,26 @@ services: - alice: + agency: image: acapy-test ports: - "3001:3001" command: > start - --label Alice + --label Agency --inbound-transport http 0.0.0.0 3000 --outbound-transport http - --endpoint http://alice:3000 + --endpoint http://agency: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 alice + --wallet-type askar + --wallet-name agency --wallet-key insecure --auto-provision + --multitenant + --multitenant-admin + --jwt-secret insecure + --multitenancy-config wallet_type=single-wallet-askar key_derivation_method=RAW --log-level info --debug-webhooks --notify-revocation @@ -30,22 +34,50 @@ tails: condition: service_started - bob: + holder_anoncreds: image: acapy-test ports: - "3002:3001" command: > start - --label Bob + --label Holder-Anoncreds --inbound-transport http 0.0.0.0 3000 --outbound-transport http - --endpoint http://bob:3000 + --endpoint http://holder_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 + --wallet-name holder_anoncreds + --wallet-key insecure + --auto-provision + --log-level info + --debug-webhooks + --monitor-revocation-notification + 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 + + holder_indy: + image: acapy-test + ports: + - "3003:3001" + command: > + start + --label Holder-Indy + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://holder_indy: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 holder_indy --wallet-key insecure --auto-provision --log-level info @@ -63,15 +95,18 @@ build: context: ../.. environment: - - ALICE=http://alice:3001 - - BOB=http://bob:3001 + - AGENCY=http://agency:3001 + - HOLDER_ANONCREDS=http://holder_anoncreds:3001 + - HOLDER_INDY=http://holder_indy:3001 volumes: - ./example.py:/usr/src/app/example.py:ro,z command: python -m example depends_on: - alice: + agency: + condition: service_healthy + holder_anoncreds: condition: service_healthy - bob: + holder_indy: condition: service_healthy tails: diff --git a/scenarios/examples/anoncreds_issuance_and_revocation/example.py b/scenarios/examples/anoncreds_issuance_and_revocation/example.py index 6d7d1a1bd2..05a68c395a 100644 --- a/scenarios/examples/anoncreds_issuance_and_revocation/example.py +++ b/scenarios/examples/anoncreds_issuance_and_revocation/example.py @@ -7,23 +7,30 @@ import json from dataclasses import dataclass from os import getenv -from secrets import token_hex +from secrets import randbelow, 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 +from acapy_controller.controller import Minimal, MinType from acapy_controller.logging import logging_to_stdout -from acapy_controller.models import V20PresExRecord, V20PresExRecordList +from acapy_controller.models import ( + CreateWalletResponse, + V20CredExRecordIndy, + V20PresExRecord, + V20PresExRecordList, +) from acapy_controller.protocols import ( DIDResult, didexchange, - indy_issue_credential_v2, - indy_present_proof_v2, + indy_anoncred_credential_artifacts, params, ) from aiohttp import ClientSession -ALICE = getenv("ALICE", "http://alice:3001") -BOB = getenv("BOB", "http://bob:3001") +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: @@ -33,7 +40,9 @@ def summary(presentation: V20PresExRecord) -> str: { "state": presentation.state, "verified": presentation.verified, - "presentation_request": request.dict(by_alias=True) if request else None, + "presentation_request": request.model_dump(by_alias=True) + if request + else None, }, indent=2, sort_keys=True, @@ -41,32 +50,395 @@ def summary(presentation: V20PresExRecord) -> str: @dataclass -class SchemaResult(Minimal): +class SchemaResultAnoncreds(Minimal): """Schema result.""" schema_state: dict @dataclass -class CredDefResult(Minimal): +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.""" - async with Controller(base_url=ALICE) as alice, Controller(base_url=BOB) as bob: + issuer_name = "issuer" + token_hex(8) + async with Controller(base_url=AGENCY) as agency: + issuer = await agency.post( + "/multitenancy/wallet", + json={ + "label": issuer_name, + "wallet_name": issuer_name, + "wallet_type": "askar", + }, + response=CreateWalletResponse, + ) + + async with Controller( + base_url=AGENCY, + wallet_id=issuer.wallet_id, + subwallet_token=issuer.token, + ) as issuer, Controller(base_url=HOLDER_ANONCREDS) as holder_anoncreds, Controller( + base_url=HOLDER_INDY + ) as holder_indy: + """ + This section of the test script demonstrates the issuance, presentation and + revocation of a credential where both the issuer is not anoncreds capable + (wallet type askar) and the holder is anoncreds capable + (wallet type askar-anoncreds). + """ + # Connecting - alice_conn, bob_conn = await didexchange(alice, bob) + issuer_conn_with_anoncreds_holder, holder_anoncreds_conn = await didexchange( + issuer, holder_anoncreds + ) # Issuance prep - config = (await alice.get("/status/config"))["config"] + config = (await issuer.get("/status/config"))["config"] genesis_url = config.get("ledger.genesis_url") - public_did = (await alice.get("/wallet/did/public", response=DIDResult)).result + public_did = (await issuer.get("/wallet/did/public", response=DIDResult)).result if not public_did: public_did = ( - await alice.post( + await issuer.post( "/wallet/did/create", json={"method": "sov", "options": {"key_type": "ed25519"}}, response=DIDResult, @@ -87,21 +459,154 @@ async def main(): ) as resp: assert resp.ok - await alice.post("/wallet/did/public", params=params(did=public_did.did)) + await issuer.post("/wallet/did/public", params=params(did=public_did.did)) + + _, cred_def = await indy_anoncred_credential_artifacts( + issuer, + ["firstname", "lastname"], + support_revocation=True, + ) + + # Issue a credential + issuer_cred_ex, _ = await issue_credential_v2( + issuer, + holder_anoncreds, + issuer_conn_with_anoncreds_holder.connection_id, + holder_anoncreds_conn.connection_id, + cred_def.credential_definition_id, + {"firstname": "Anoncreds", "lastname": "Holder"}, + ) + + # Present the the credential's attributes + await present_proof_v2( + holder_anoncreds, + issuer, + holder_anoncreds_conn.connection_id, + issuer_conn_with_anoncreds_holder.connection_id, + requested_attributes=[{"name": "firstname"}], + ) + + # Revoke credential + await issuer.post( + url="/revocation/revoke", + json={ + "connection_id": issuer_conn_with_anoncreds_holder.connection_id, + "rev_reg_id": issuer_cred_ex.details.rev_reg_id, + "cred_rev_id": issuer_cred_ex.details.cred_rev_id, + "publish": True, + "notify": True, + "notify_version": "v1_0", + }, + ) + + await holder_anoncreds.record(topic="revocation-notification") + + """ + This section of the test script demonstrates the issuance, presentation and + revocation of a credential where the issuer and holder are not anoncreds + capable. Both are askar wallet type. + """ + + # Connecting + issuer_conn_with_indy_holder, holder_indy_conn = await didexchange( + issuer, holder_indy + ) + + # Issue a credential + issuer_cred_ex, _ = await issue_credential_v2( + issuer, + holder_indy, + issuer_conn_with_indy_holder.connection_id, + holder_indy_conn.connection_id, + cred_def.credential_definition_id, + {"firstname": "Indy", "lastname": "Holder"}, + ) + + # Present the the credential's attributes + await present_proof_v2( + holder_indy, + issuer, + holder_indy_conn.connection_id, + issuer_conn_with_indy_holder.connection_id, + requested_attributes=[{"name": "firstname"}], + ) + # Query presentations + presentations = await issuer.get( + "/present-proof-2.0/records", + response=V20PresExRecordList, + ) + + # Presentation summary + for _, pres in enumerate(presentations.results): + print(summary(pres)) + + # Revoke credential + await issuer.post( + url="/revocation/revoke", + json={ + "connection_id": issuer_conn_with_indy_holder.connection_id, + "rev_reg_id": issuer_cred_ex.details.rev_reg_id, + "cred_rev_id": issuer_cred_ex.details.cred_rev_id, + "publish": True, + "notify": True, + "notify_version": "v1_0", + }, + ) + + await holder_indy.record(topic="revocation-notification") - schema = await alice.post( + """ + Upgrade the issuer tenant to anoncreds capable wallet type. When upgrading a + tenant the agent doesn't require a restart. That is why the test is done + with multitenancy + """ + await issuer.post( + "/anoncreds/wallet/upgrade", + params={ + "wallet_name": issuer_name, + }, + ) + + # Wait for the upgrade to complete + await asyncio.sleep(2) + + """ + Do issuance and presentation again after the upgrade. This time the issuer is + an anoncreds capable wallet (wallet type askar-anoncreds). + """ + # Presentation for anoncreds capable holder on existing credential + await present_proof_v2( + holder_anoncreds, + issuer, + holder_anoncreds_conn.connection_id, + issuer_conn_with_anoncreds_holder.connection_id, + requested_attributes=[{"name": "firstname"}], + ) + + # Presentation for indy capable holder on existing credential + await present_proof_v2( + holder_indy, + issuer, + holder_indy_conn.connection_id, + issuer_conn_with_indy_holder.connection_id, + requested_attributes=[{"name": "firstname"}], + ) + + # Create a new schema and cred def with different attributes on new + # anoncreds endpoints + schema = await issuer.post( "/anoncreds/schema", json={ "schema": { "name": "anoncreds-test-" + token_hex(8), "version": "1.0", - "attrNames": ["firstname", "lastname"], + "attrNames": ["middlename"], "issuerId": public_did.did, } }, - response=SchemaResult, + response=SchemaResultAnoncreds, ) - cred_def = await alice.post( + cred_def = await issuer.post( "/anoncreds/credential-definition", json={ "credential_definition": { @@ -109,77 +614,73 @@ async def main(): "schemaId": schema.schema_state["schema_id"], "tag": token_hex(8), }, - "options": { - "revocation_registry_size": 2000, - "support_revocation": True, - }, + "options": {"support_revocation": True, "revocation_registry_size": 10}, }, - response=CredDefResult, + response=CredDefResultAnoncreds, ) - # Issue a credential - alice_cred_ex, _ = await indy_issue_credential_v2( - alice, - bob, - alice_conn.connection_id, - bob_conn.connection_id, + # Issue a new credential to anoncreds holder + issuer_cred_ex, _ = await issue_credential_v2( + issuer, + holder_anoncreds, + issuer_conn_with_anoncreds_holder.connection_id, + holder_anoncreds_conn.connection_id, cred_def.credential_definition_state["credential_definition_id"], - {"firstname": "Bob", "lastname": "Builder"}, + {"middlename": "Anoncreds"}, ) - - # Present the the credential's attributes - await indy_present_proof_v2( - bob, - alice, - bob_conn.connection_id, - alice_conn.connection_id, - requested_attributes=[{"name": "firstname"}], + # Presentation for anoncreds capable holder + await present_proof_v2( + holder_anoncreds, + issuer, + holder_anoncreds_conn.connection_id, + issuer_conn_with_anoncreds_holder.connection_id, + requested_attributes=[{"name": "middlename"}], ) - # Revoke credential - await alice.post( + await issuer.post( url="/anoncreds/revocation/revoke", json={ - "connection_id": alice_conn.connection_id, - "rev_reg_id": alice_cred_ex.indy.rev_reg_id, - "cred_rev_id": alice_cred_ex.indy.cred_rev_id, + "connection_id": issuer_conn_with_anoncreds_holder.connection_id, + "rev_reg_id": issuer_cred_ex.details.rev_reg_id, + "cred_rev_id": issuer_cred_ex.details.cred_rev_id, "publish": True, "notify": True, "notify_version": "v1_0", }, ) + await holder_anoncreds.record(topic="revocation-notification") - await bob.record(topic="revocation-notification") - - # Request proof, no interval - await indy_present_proof_v2( - bob, - alice, - bob_conn.connection_id, - alice_conn.connection_id, - requested_attributes=[ - { - "name": "firstname", - "restrictions": [ - { - "cred_def_id": cred_def.credential_definition_state[ - "credential_definition_id" - ] - } - ], - } - ], + # Issue a new credential to indy holder + issuer_cred_ex, _ = await issue_credential_v2( + issuer, + holder_indy, + issuer_conn_with_indy_holder.connection_id, + holder_indy_conn.connection_id, + cred_def.credential_definition_state["credential_definition_id"], + {"middlename": "Indy"}, ) - - # Query presentations - presentations = await alice.get( - "/present-proof-2.0/records", - response=V20PresExRecordList, + # Presentation for indy holder + await present_proof_v2( + holder_indy, + issuer, + holder_indy_conn.connection_id, + issuer_conn_with_indy_holder.connection_id, + requested_attributes=[{"name": "middlename"}], + ) + # Revoke credential + await issuer.post( + url="/anoncreds/revocation/revoke", + json={ + "connection_id": issuer_conn_with_indy_holder.connection_id, + "rev_reg_id": issuer_cred_ex.details.rev_reg_id, + "cred_rev_id": issuer_cred_ex.details.cred_rev_id, + "publish": True, + "notify": True, + "notify_version": "v1_0", + }, ) - # Presentation summary - for i, pres in enumerate(presentations.results): - print(summary(pres)) + await holder_indy.record(topic="revocation-notification") if __name__ == "__main__": diff --git a/scenarios/examples/cheqd/docker-compose.yml b/scenarios/examples/cheqd/docker-compose.yml new file mode 100644 index 0000000000..f8dde6abe4 --- /dev/null +++ b/scenarios/examples/cheqd/docker-compose.yml @@ -0,0 +1,70 @@ + services: + issuer: + image: acapy-test + ports: + - "3001:3001" + command: > + start + --label Issuer + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://issuer:3000 + --admin 0.0.0.0 3001 + --admin-insecure-mode + --wallet-type askar-anoncreds + --wallet-name agency + --wallet-key insecure + --auto-provision + --log-level info + --debug-webhooks + --no-ledger + 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 + + holder: + image: acapy-test + ports: + - "3002:3001" + command: > + start + --label Holder + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://holder:3000 + --admin 0.0.0.0 3001 + --admin-insecure-mode + --wallet-type askar-anoncreds + --wallet-name holder + --wallet-key insecure + --auto-provision + --log-level info + --debug-webhooks + --no-ledger + 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 + + example: + container_name: controller + build: + context: ../.. + environment: + - ISSUER=http://issuer:3001 + - HOLDER=http://holder:3001 + volumes: + - ./example.py:/usr/src/app/example.py:ro,z + command: python -m example + depends_on: + issuer: + condition: service_healthy + holder: + condition: service_healthy + + diff --git a/scenarios/examples/cheqd/example.py b/scenarios/examples/cheqd/example.py new file mode 100644 index 0000000000..1e141b4871 --- /dev/null +++ b/scenarios/examples/cheqd/example.py @@ -0,0 +1,332 @@ +"""Minimal reproducible example script. + +This script is for you to use to reproduce a bug or demonstrate a feature. +""" + +import asyncio +import json +from dataclasses import dataclass +from os import getenv +from typing import Tuple, Mapping, Optional + +from acapy_controller import Controller +from acapy_controller.logging import logging_to_stdout +from acapy_controller.protocols import didexchange +from acapy_controller.controller import Minimal + +ISSUER = getenv("ISSUER", "http://issuer:3001") +HOLDER = getenv("HOLDER", "http://holder:3001") + + +@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 + + +def format_json(json_to_format): + """Pretty print json.""" + return json.dumps(json_to_format, indent=4) + + +async def create_did(issuer): + """Create a DID on the Cheqd testnet.""" + did_create_result = await issuer.post("/did/cheqd/create") + did = did_create_result.get("did") + + assert did, "DID creation failed." + assert did_create_result.get("verkey"), "Verkey is missing in DID creation result." + + print(f"Created DID: {did}") + return did + + +async def resolve_did(issuer, did): + """Resolve the DID document.""" + resolution_result = await issuer.get(f"/resolver/resolve/{did}") + did_document = resolution_result.get("did_document") + + assert did_document, "DID document resolution failed." + print(f"Resolved DID Document: {format_json(did_document)}") + return did_document + + +async def update_did(issuer, did, did_document): + """Update the DID document by adding a service endpoint.""" + service = [ + { + "id": f"{did}#service-1", + "type": "MessagingService", + "serviceEndpoint": ["https://example.com/service"], + } + ] + did_document["service"] = service + del did_document["@context"] + + did_update_result = await issuer.post( + "/did/cheqd/update", json={"did": did, "didDocument": did_document} + ) + updated_did_doc = did_update_result.get("didDocument") + updated_did = did_update_result.get("did") + + assert updated_did == did, "DID mismatch after update." + assert ( + "service" in updated_did_doc + ), "Key 'service' is missing in updated DID document." + assert ( + updated_did_doc["service"] == service + ), "Service does not match the expected value!" + + print(f"Updated DID Document: {format_json(updated_did_doc)}") + return updated_did_doc + + +async def deactivate_did(issuer, did): + """Deactivate a DID on the Cheqd testnet.""" + did_deactivate_result = await issuer.post( + "/did/cheqd/deactivate", + json={ + "did": did, + "options": {"network": "testnet"}, + }, + ) + + assert did_deactivate_result.get("did") == did, "DID mismatch after deactivation." + assert ( + did_deactivate_result.get("did_document_metadata", {}).get("deactivated") is True + ), "DID document metadata does not contain deactivated=true." + + print(f"Deactivated DID: {format_json(did_deactivate_result) }") + + +async def create_schema(issuer, did): + """Create a schema on the Cheqd testnet.""" + schema_create_result = await issuer.post( + "/anoncreds/schema", + json={ + "schema": { + "attrNames": ["score"], + "issuerId": did, + "name": "Example schema", + "version": "1.0", + } + }, + ) + print(f"Created schema: {format_json(schema_create_result)}") + schema_state = schema_create_result.get("schema_state") + assert schema_state.get("state") == "finished", "Schema state is not finished." + assert "schema_id" in schema_state, "Key 'schema_id' is missing in schema_state." + + schema_id = schema_state.get("schema_id") + assert ( + did in schema_id + ), f"schema_id does not contain the expected DID. Expected '{did}' in '{schema_id}'." + + return schema_id + + +async def create_credential_definition(issuer, did, schema_id): + """Create a credential definition on the connected datastore.""" + cred_def_create_result = await issuer.post( + "/anoncreds/credential-definition", + json={ + "credential_definition": { + "issuerId": did, + "schemaId": schema_id, + "tag": "default", + } + }, + ) + + cred_def_state = cred_def_create_result.get("credential_definition_state", {}) + assert cred_def_state.get("state") == "finished", "Cred def state is not finished." + assert ( + "credential_definition_id" in cred_def_state + ), "Key 'credential_definition_id' is missing in credential_definition_state." + + credential_definition_id = cred_def_state.get("credential_definition_id") + assert ( + did in credential_definition_id + ), "credential_definition_id does not contain the expected DID." + + print(f"Created credential definition: {format_json(cred_def_create_result)}") + return credential_definition_id + + +async def assert_credential_definitions(issuer, credential_definition_id): + """Retrieve all cred_defs & ensure array contain created credential_definition_id.""" + get_result = await issuer.get("/anoncreds/credential-definitions") + + credential_definition_ids = get_result.get("credential_definition_ids", []) + assert ( + credential_definition_id in credential_definition_ids + ), "credential_definition_ids does not contain the expected credential_definition_id." + + +async def assert_wallet_dids(issuer, did): + """Retrieve all wallet dids and ensure array contain created did.""" + get_result = await issuer.get("/wallet/did?method=cheqd") + + dids = get_result.get("results", []) + assert any(obj.get("did") == did for obj in dids), f"DID {did} not found in array" + + +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. + """ + + issuer_cred_ex = await issuer.post( + "/issue-credential-2.0/send", + json={ + "auto_issue": True, + "auto_remove": False, + "comment": "Credential from minimal example", + "trace": False, + "connection_id": issuer_connection_id, + "filter": { + "anoncreds": { + "cred_def_id": cred_def_id, + } + }, + "credential_preview": { + "type": "issue-credential-2.0/2.0/credential-preview", # pyright: ignore + "attributes": [ + { + "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 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", + ) + + 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", + ) + + return ( + V20CredExRecordDetail(cred_ex_record=issuer_cred_ex), + V20CredExRecordDetail(cred_ex_record=holder_cred_ex), + ) + + +async def main(): + """Test DID Cheqd workflow.""" + async with Controller(base_url=ISSUER) as issuer, Controller( + base_url=HOLDER + ) as holder: + """ + This section of the test script demonstrates the CRUD operations of a did + followed by creating schema, credential definition and credential issuance. + """ + did = await create_did(issuer) + + await resolve_did(issuer, did) + + # updated_did_document = await update_did(issuer, did, did_document) + + schema_id = await create_schema(issuer, did) + print(schema_id) + + credential_definition_id = await create_credential_definition( + issuer, did, schema_id + ) + print(credential_definition_id) + + await assert_credential_definitions(issuer, credential_definition_id) + await assert_wallet_dids(issuer, did) + + # Connect issuer and holder + issuer_conn_with_anoncreds_holder, holder_anoncreds_conn = await didexchange( + issuer, holder + ) + + issue_credential_result = await issue_credential_v2( + issuer, + holder, + issuer_conn_with_anoncreds_holder.connection_id, + holder_anoncreds_conn.connection_id, + credential_definition_id, + {"score": "99"}, + ) + print(issue_credential_result) + + # await deactivate_did(issuer, did) + + +if __name__ == "__main__": + logging_to_stdout() + asyncio.run(main())