diff --git a/acapy_agent/anoncreds/base.py b/acapy_agent/anoncreds/base.py index 32c0a1c1f1..4c5b79d345 100644 --- a/acapy_agent/anoncreds/base.py +++ b/acapy_agent/anoncreds/base.py @@ -16,6 +16,7 @@ RevRegDefResult, ) from .models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult +from .models.schema_info import AnoncredsSchemaInfo T = TypeVar("T") @@ -130,9 +131,15 @@ async def get_revocation_list( ) -> GetRevListResult: """Get a revocation list from the registry.""" + @abstractmethod + async def get_schema_info_by_id( + self, profile: Profile, schema_id: str + ) -> AnoncredsSchemaInfo: + """Get a schema info from the registry.""" + class BaseAnonCredsRegistrar(BaseAnonCredsHandler): - """Base Anon Creds Registrar.""" + """Base Anoncreds Registrar.""" @abstractmethod async def register_schema( diff --git a/acapy_agent/anoncreds/default/did_indy/registry.py b/acapy_agent/anoncreds/default/did_indy/registry.py index dcaafe4c06..6bd6cae693 100644 --- a/acapy_agent/anoncreds/default/did_indy/registry.py +++ b/acapy_agent/anoncreds/default/did_indy/registry.py @@ -17,6 +17,7 @@ RevRegDefResult, ) from ...models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult +from ...models.schema_info import AnoncredsSchemaInfo LOGGER = logging.getLogger(__name__) @@ -118,3 +119,9 @@ async def update_revocation_list( ) -> RevListResult: """Update a revocation list on the registry.""" raise NotImplementedError() + + async def get_schema_info_by_id( + self, profile: Profile, schema_id: str + ) -> AnoncredsSchemaInfo: + """Get a schema info from the registry.""" + return await super().get_schema_info_by_id(schema_id) diff --git a/acapy_agent/anoncreds/default/did_web/registry.py b/acapy_agent/anoncreds/default/did_web/registry.py index f97ba88fb8..718b624842 100644 --- a/acapy_agent/anoncreds/default/did_web/registry.py +++ b/acapy_agent/anoncreds/default/did_web/registry.py @@ -16,6 +16,7 @@ RevRegDefResult, ) from ...models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult +from ...models.schema_info import AnoncredsSchemaInfo class DIDWebRegistry(BaseAnonCredsResolver, BaseAnonCredsRegistrar): @@ -113,3 +114,9 @@ async def update_revocation_list( ) -> RevListResult: """Update a revocation list on the registry.""" raise NotImplementedError() + + async def get_schema_info_by_id( + self, profile: Profile, schema_id: str + ) -> AnoncredsSchemaInfo: + """Get a schema info from the registry.""" + return await super().get_schema_info_by_id(schema_id) diff --git a/acapy_agent/anoncreds/default/legacy_indy/registry.py b/acapy_agent/anoncreds/default/legacy_indy/registry.py index aff1040616..aad288d053 100644 --- a/acapy_agent/anoncreds/default/legacy_indy/registry.py +++ b/acapy_agent/anoncreds/default/legacy_indy/registry.py @@ -80,6 +80,7 @@ SchemaResult, SchemaState, ) +from ...models.schema_info import AnoncredsSchemaInfo from ...revocation import ( CATEGORY_REV_LIST, CATEGORY_REV_REG_DEF, @@ -1229,3 +1230,14 @@ async def txn_submit( ) except LedgerError as err: raise AnonCredsRegistrationError(err.roll_up) from err + + async def get_schema_info_by_id( + self, profile: Profile, schema_id: str + ) -> AnoncredsSchemaInfo: + """Get schema info by schema id.""" + schema_id_parts = re.match(r"^(\w+):2:([^:]+):([^:]+)$", schema_id) + return AnoncredsSchemaInfo( + issuer_id=schema_id_parts.group(1), + name=schema_id_parts.group(2), + version=schema_id_parts.group(3), + ) 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 c41eb80797..76ebbee7a2 100644 --- a/acapy_agent/anoncreds/default/legacy_indy/tests/test_registry.py +++ b/acapy_agent/anoncreds/default/legacy_indy/tests/test_registry.py @@ -1210,3 +1210,12 @@ async def test_sync_wallet_rev_list_with_issuer_cred_rev_records( ), ) assert isinstance(result, RevList) + + async def test_get_schem_info(self): + result = await self.registry.get_schema_info_by_id( + self.profile, + "XduBsoPyEA4szYMy3pZ8De:2:minimal-33279d005748b3cc:1.0", + ) + assert result.issuer_id == "XduBsoPyEA4szYMy3pZ8De" + assert result.name == "minimal-33279d005748b3cc" + assert result.version == "1.0" diff --git a/acapy_agent/anoncreds/holder.py b/acapy_agent/anoncreds/holder.py index f8dd4445f7..08006fe176 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 ( @@ -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,19 @@ 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}" - ) + registry = self.profile.inject(AnonCredsRegistry) + schema_info = await registry.get_schema_info_by_id( + self.profile, credential_data["schema_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": credential_data["schema_id"], + "schema_issuer_did": schema_info.issuer_id, + "schema_name": schema_info.name, + "schema_version": schema_info.version, + "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/models/credential_request.py b/acapy_agent/anoncreds/models/credential_request.py index 49fd58e996..2d5147b4a6 100644 --- a/acapy_agent/anoncreds/models/credential_request.py +++ b/acapy_agent/anoncreds/models/credential_request.py @@ -24,6 +24,8 @@ class Meta: def __init__( self, + entropy: Optional[str] = None, + # For compatibility with credx agents, which uses `prover_did` instead of `entropy` # noqa prover_did: Optional[str] = None, cred_def_id: Optional[str] = None, blinded_ms: Optional[Mapping] = None, @@ -33,6 +35,7 @@ def __init__( ): """Initialize anoncreds credential request.""" super().__init__(**kwargs) + self.entropy = entropy self.prover_did = prover_did self.cred_def_id = cred_def_id self.blinded_ms = blinded_ms @@ -49,8 +52,16 @@ class Meta: model_class = AnoncredsCredRequest unknown = EXCLUDE + entropy = fields.Str( + required=False, + metadata={ + "description": "Prover DID/Random String/UUID", + "example": UUID4_EXAMPLE, + }, + ) + # For compatibility with credx agents, which uses `prover_did` instead of `entropy` prover_did = fields.Str( - required=True, + required=False, metadata={ "description": "Prover DID/Random String/UUID", "example": UUID4_EXAMPLE, diff --git a/acapy_agent/anoncreds/models/schema_info.py b/acapy_agent/anoncreds/models/schema_info.py new file mode 100644 index 0000000000..e5cda7100d --- /dev/null +++ b/acapy_agent/anoncreds/models/schema_info.py @@ -0,0 +1,26 @@ +"""This class represents schema information for anoncreds.""" + +from typing import Optional + + +class AnoncredsSchemaInfo: + """Represents the schema information for anonymous credentials. + + Attributes: + issuer_id (str): The identifier of the issuer. + name (Optional[str]): The name of the schema. Defaults to None. + version (Optional[str]): The version of the schema. Defaults to None. + + Args: + issuer_id (str): The identifier of the issuer. + name (Optional[str], optional): The name of the schema. Defaults to None. + version (Optional[str], optional): The version of the schema. Defaults to None. + """ + + def __init__( + self, issuer_id: str, name: Optional[str] = None, version: Optional[str] = None + ): + """Initialize the schema information.""" + self.issuer_id = issuer_id + self.name = name + self.version = version diff --git a/acapy_agent/anoncreds/registry.py b/acapy_agent/anoncreds/registry.py index c355b447cd..92b70ac516 100644 --- a/acapy_agent/anoncreds/registry.py +++ b/acapy_agent/anoncreds/registry.py @@ -21,6 +21,7 @@ RevRegDefResult, ) from .models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult +from .models.schema_info import AnoncredsSchemaInfo LOGGER = logging.getLogger(__name__) @@ -99,6 +100,13 @@ async def get_credential_definition( credential_definition_id, ) + async def get_schema_info_by_id( + self, profile: Profile, schema_id: str + ) -> AnoncredsSchemaInfo: + """Get a schema info from the registry.""" + resolver = await self._resolver_for_identifier(schema_id) + return await resolver.get_schema_info_by_id(profile, schema_id) + async def register_credential_definition( self, profile: Profile, diff --git a/acapy_agent/anoncreds/tests/test_holder.py b/acapy_agent/anoncreds/tests/test_holder.py index 34d23ef75f..9667fe76e5 100644 --- a/acapy_agent/anoncreds/tests/test_holder.py +++ b/acapy_agent/anoncreds/tests/test_holder.py @@ -55,11 +55,6 @@ def __init__(self, bad_schema=False, bad_cred_def=False): self.schema_id = "Sc886XPwD1gDcHwmmLDeR2:2:degree schema:45.101.94" self.cred_def_id = "Sc886XPwD1gDcHwmmLDeR2:3:CL:229975:faber.agent.degree_schema" - if bad_schema: - self.schema_id = "bad-schema-id" - if bad_cred_def: - self.cred_def_id = "bad-cred-def-id" - schema_id = "Sc886XPwD1gDcHwmmLDeR2:2:degree schema:45.101.94" cred_def_id = "Sc886XPwD1gDcHwmmLDeR2:3:CL:229975:faber.agent.degree_schema" rev_reg_id = None @@ -72,15 +67,10 @@ def to_dict(self): class MockCredReceivedW3C: - def __init__(self, bad_schema=False, bad_cred_def=False): + def __init__(self): self.schema_id = "Sc886XPwD1gDcHwmmLDeR2:2:degree schema:45.101.94" self.cred_def_id = "Sc886XPwD1gDcHwmmLDeR2:3:CL:229975:faber.agent.degree_schema" - if bad_schema: - self.schema_id = "bad-schema-id" - if bad_cred_def: - self.cred_def_id = "bad-cred-def-id" - def to_json_buffer(self): return b"credential" @@ -89,9 +79,7 @@ def to_dict(self): class MockCredential: - def __init__(self, bad_schema=False, bad_cred_def=False): - self.bad_schema = bad_schema - self.bad_cred_def = bad_cred_def + def __init__(self): self.rev_reg_id = "rev-reg-id" self.rev_reg_index = 0 @@ -101,21 +89,17 @@ def to_dict(self): return MOCK_CRED def process(self, *args, **kwargs): - return MockCredReceived(self.bad_schema, self.bad_cred_def) + return MockCredReceived() class MockW3Credential: - def __init__(self, bad_schema=False, bad_cred_def=False): - self.bad_schema = bad_schema - self.bad_cred_def = bad_cred_def - cred = mock.AsyncMock(auto_spec=W3cCredential) def to_dict(self): return MOCK_W3C_CRED def process(self, *args, **kwargs): - return MockCredReceivedW3C(self.bad_schema, self.bad_cred_def) + return MockCredReceivedW3C() class MockMasterSecret: @@ -285,8 +269,6 @@ async def test_store_credential_fails_to_load_raises_x(self, mock_master_secret) side_effect=[ MockCredential(), MockCredential(), - MockCredential(bad_schema=True), - MockCredential(bad_cred_def=True), ], ) async def test_store_credential(self, mock_load, mock_master_secret): @@ -296,6 +278,9 @@ async def test_store_credential(self, mock_load, mock_master_secret): commit=mock.CoroutineMock(return_value=None), ) ) + self.profile.context.injector.bind_instance( + AnonCredsRegistry, mock.MagicMock(AnonCredsRegistry, autospec=True) + ) # Valid result = await self.holder.store_credential( @@ -321,20 +306,6 @@ async def test_store_credential(self, mock_load, mock_master_secret): {"cred-req-meta": "cred-req-meta"}, ) - # Test bad id's - with self.assertRaises(AnonCredsHolderError): - await self.holder.store_credential( - MOCK_CRED_DEF, - MOCK_PRES, - {"cred-req-meta": "cred-req-meta"}, - ) - with self.assertRaises(AnonCredsHolderError): - await self.holder.store_credential( - MOCK_CRED_DEF, - MOCK_CRED, - {"cred-req-meta": "cred-req-meta"}, - ) - @mock.patch.object(AnonCredsHolder, "get_master_secret", return_value="master-secret") @mock.patch.object( W3cCredential, @@ -362,7 +333,9 @@ async def test_store_credential_w3c( commit=mock.CoroutineMock(return_value=None), ) ) - + self.profile.context.injector.bind_instance( + AnonCredsRegistry, mock.MagicMock(AnonCredsRegistry, autospec=True) + ) with mock.patch.object(jsonld, "expand", return_value=MagicMock()): with mock.patch.object(JsonLdProcessor, "get_values", return_value=["type1"]): result = await self.holder.store_credential_w3c( @@ -384,6 +357,9 @@ async def test_store_credential_failed_trx(self, *_): self.profile.transaction = mock.MagicMock( side_effect=[AskarError(AskarErrorCode.UNEXPECTED, "test")] ) + self.profile.context.injector.bind_instance( + AnonCredsRegistry, mock.MagicMock(AnonCredsRegistry, autospec=True) + ) with self.assertRaises(AnonCredsHolderError): await self.holder.store_credential( diff --git a/acapy_agent/indy/credx/issuer.py b/acapy_agent/indy/credx/issuer.py index 8cd857df9e..c67b4d911a 100644 --- a/acapy_agent/indy/credx/issuer.py +++ b/acapy_agent/indy/credx/issuer.py @@ -330,6 +330,11 @@ async def create_credential( revoc = None credential_revocation_id = None + # This is for compatibility with an anoncreds holder + if not credential_request.get("prover_did"): + credential_request["prover_did"] = credential_request["entropy"] + del credential_request["entropy"] + try: ( credential, 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 f795929e8b..07f2d948c4 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 @@ -132,7 +132,7 @@ "nonce": "1234567890", } ANONCREDS_CRED_REQ = { - "prover_did": TEST_DID, + "entropy": TEST_DID, "cred_def_id": CRED_DEF_ID, "blinded_ms": { "u": "12345", 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 2c19d13443..19b3264687 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 @@ -11,6 +11,7 @@ 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.registry import AnonCredsRegistry from ......anoncreds.util import generate_pr_nonce from ......anoncreds.verifier import AnonCredsVerifier from ......messaging.decorators.attach_decorator import AttachDecorator @@ -175,7 +176,7 @@ async def create_pres( 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(): + async def _check_proof_vs_proposal(): """Check for bait and switch in presented values vs. proposal request.""" from ..indy.handler import IndyPresExchangeHandler @@ -198,13 +199,18 @@ def _check_proof_vs_proposal(): sub_proof_index = attr_spec["sub_proof_index"] schema_id = proof["identifiers"][sub_proof_index]["schema_id"] cred_def_id = proof["identifiers"][sub_proof_index]["cred_def_id"] + registry = self.profile.inject(AnonCredsRegistry) + schema = await registry.get_schema(self.profile, schema_id) + cred_def = await registry.get_credential_definition( + self.profile, cred_def_id + ) criteria = { "schema_id": schema_id, - "schema_issuer_did": schema_id.split(":")[-4], - "schema_name": schema_id.split(":")[-2], - "schema_version": schema_id.split(":")[-1], + "schema_issuer_did": schema.schema_value.issuer_id, + "schema_name": schema.schema_value.name, + "schema_version": schema.schema_value.version, "cred_def_id": cred_def_id, - "issuer_did": cred_def_id.split(":")[-5], + "issuer_did": cred_def.credential_definition.issuer_id, f"attr::{name}::value": proof_value, } @@ -233,13 +239,18 @@ def _check_proof_vs_proposal(): sub_proof_index = attr_spec["sub_proof_index"] schema_id = proof["identifiers"][sub_proof_index]["schema_id"] cred_def_id = proof["identifiers"][sub_proof_index]["cred_def_id"] + registry = self.profile.inject(AnonCredsRegistry) + schema = await registry.get_schema(self.profile, schema_id) + cred_def = await registry.get_credential_definition( + self.profile, cred_def_id + ) criteria = { "schema_id": schema_id, - "schema_issuer_did": schema_id.split(":")[-4], - "schema_name": schema_id.split(":")[-2], - "schema_version": schema_id.split(":")[-1], + "schema_issuer_did": schema.schema_value.issuer_id, + "schema_name": schema.schema_value.name, + "schema_version": schema.schema_value.version, "cred_def_id": cred_def_id, - "issuer_did": cred_def_id.split(":")[-5], + "issuer_did": cred_def.credential_definition.issuer_id, **{ f"attr::{name}::value": value for name, value in proof_values.items() @@ -294,13 +305,18 @@ def _check_proof_vs_proposal(): schema_id = proof["identifiers"][sub_proof_index]["schema_id"] cred_def_id = proof["identifiers"][sub_proof_index]["cred_def_id"] + registry = self.profile.inject(AnonCredsRegistry) + schema = await registry.get_schema(self.profile, schema_id) + cred_def = await registry.get_credential_definition( + self.profile, cred_def_id + ) criteria = { "schema_id": schema_id, - "schema_issuer_did": schema_id.split(":")[-4], - "schema_name": schema_id.split(":")[-2], - "schema_version": schema_id.split(":")[-1], + "schema_issuer_did": schema.schema_value.issuer_id, + "schema_name": schema.schema_value.name, + "schema_version": schema.schema_value.version, "cred_def_id": cred_def_id, - "issuer_did": cred_def_id.split(":")[-5], + "issuer_did": cred_def.credential_definition.issuer_id, } if ( @@ -313,7 +329,7 @@ def _check_proof_vs_proposal(): ) proof = message.attachment(AnonCredsPresExchangeHandler.format) - _check_proof_vs_proposal() + await _check_proof_vs_proposal() async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: """Verify a presentation. 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 9f88377968..a836e55ab3 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 @@ -6,6 +6,14 @@ import pytest from .....anoncreds.holder import AnonCredsHolder +from .....anoncreds.models.credential_definition import ( + CredDef, + CredDefValue, + CredDefValuePrimary, + GetCredDefResult, +) +from .....anoncreds.models.schema import AnonCredsSchema, GetSchemaResult +from .....anoncreds.registry import AnonCredsRegistry from .....anoncreds.verifier import AnonCredsVerifier from .....indy.models.pres_preview import ( IndyPresAttrSpec, @@ -469,6 +477,37 @@ async def asyncSetUp(self): ) ) injector.bind_instance(AnonCredsHolder, self.holder) + registry = mock.MagicMock(AnonCredsRegistry, autospec=True) + registry.get_schema = mock.CoroutineMock( + return_value=GetSchemaResult( + schema=AnonCredsSchema( + issuer_id=ISSUER_DID, + name="vidya", + version="1.0", + attr_names=["player", "screenCapture", "highScore"], + ), + schema_id=S_ID, + resolution_metadata={}, + schema_metadata={}, + ) + ) + registry.get_credential_definition = mock.CoroutineMock( + return_value=GetCredDefResult( + credential_definition_id="TUku9MDGa7QALbAJX4oAww:3:CL:531757:MYCO_Consent_Enablement", + credential_definition=CredDef( + issuer_id=ISSUER_DID, + schema_id=S_ID, + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z") + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ) + ) + injector.bind_instance(AnonCredsRegistry, registry) self.verifier = mock.MagicMock(AnonCredsVerifier, autospec=True) self.verifier.verify_presentation = mock.CoroutineMock(return_value=("true", []))