From e08c47fd33b093a33cc68d2af7a167b3da1e3f6a Mon Sep 17 00:00:00 2001 From: jamshale Date: Fri, 6 Dec 2024 22:23:47 +0000 Subject: [PATCH] Fix anoncreds issuance and compatibility Signed-off-by: jamshale --- acapy_agent/anoncreds/base.py | 4 ++ .../anoncreds/default/did_indy/registry.py | 4 ++ .../anoncreds/default/did_web/registry.py | 4 ++ .../anoncreds/default/legacy_indy/registry.py | 9 ++++ acapy_agent/anoncreds/holder.py | 27 ++++------ .../anoncreds/models/credential_request.py | 13 ++++- .../anoncreds/models/presentation_request.py | 12 ++--- acapy_agent/anoncreds/registry.py | 5 ++ acapy_agent/anoncreds/tests/test_holder.py | 50 +++++-------------- acapy_agent/indy/credx/issuer.py | 5 ++ .../formats/anoncreds/tests/test_handler.py | 2 +- .../v2_0/tests/test_manager_anoncreds.py | 2 +- 12 files changed, 73 insertions(+), 64 deletions(-) diff --git a/acapy_agent/anoncreds/base.py b/acapy_agent/anoncreds/base.py index 32c0a1c1f1..26ad015713 100644 --- a/acapy_agent/anoncreds/base.py +++ b/acapy_agent/anoncreds/base.py @@ -130,6 +130,10 @@ async def get_revocation_list( ) -> GetRevListResult: """Get a revocation list from the registry.""" + @abstractmethod + async def get_schema_info_by_id(self, schema_id: str) -> dict: + """Get a schema info from the registry.""" + class BaseAnonCredsRegistrar(BaseAnonCredsHandler): """Base Anon Creds Registrar.""" diff --git a/acapy_agent/anoncreds/default/did_indy/registry.py b/acapy_agent/anoncreds/default/did_indy/registry.py index dcaafe4c06..687385f9d8 100644 --- a/acapy_agent/anoncreds/default/did_indy/registry.py +++ b/acapy_agent/anoncreds/default/did_indy/registry.py @@ -118,3 +118,7 @@ async def update_revocation_list( ) -> RevListResult: """Update a revocation list on the registry.""" raise NotImplementedError() + + async def get_schema_info_by_id(self, schema_id: str) -> dict: + """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..2b2dcbf27f 100644 --- a/acapy_agent/anoncreds/default/did_web/registry.py +++ b/acapy_agent/anoncreds/default/did_web/registry.py @@ -113,3 +113,7 @@ async def update_revocation_list( ) -> RevListResult: """Update a revocation list on the registry.""" raise NotImplementedError() + + async def get_schema_info_by_id(self, schema_id: str) -> dict: + """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..f0cbaee780 100644 --- a/acapy_agent/anoncreds/default/legacy_indy/registry.py +++ b/acapy_agent/anoncreds/default/legacy_indy/registry.py @@ -1229,3 +1229,12 @@ async def txn_submit( ) except LedgerError as err: raise AnonCredsRegistrationError(err.roll_up) from err + + async def get_schema_info_by_id(self, schema_id: str) -> dict: + """Get schema info by schema id.""" + schema_id_parts = re.match(r"^(\w+):2:([^:]+):([^:]+)$", schema_id) + return { + "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/holder.py b/acapy_agent/anoncreds/holder.py index f8dd4445f7..d7543b2ed9 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,17 @@ 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(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_id": schema_info["issuer_id"], + "schema_name": schema_info["name"], + "schema_version": schema_info["version"], + "issuer_id": 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/presentation_request.py b/acapy_agent/anoncreds/models/presentation_request.py index 12855e1ee6..1d7c238064 100644 --- a/acapy_agent/anoncreds/models/presentation_request.py +++ b/acapy_agent/anoncreds/models/presentation_request.py @@ -47,7 +47,7 @@ class AnoncredsPresentationReqPredSpecSchema(OpenAPISchema): fields.Dict( keys=fields.Str( validate=validate.Regexp( - "^schema_id|schema_issuer_did|schema_name|schema_version|issuer_did|" + "^schema_id|schema_issuer_id|schema_name|schema_version|issuer_id|" "cred_def_id|attr::.+::value$" ), metadata={"example": "cred_def_id"}, @@ -58,8 +58,8 @@ class AnoncredsPresentationReqPredSpecSchema(OpenAPISchema): 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" + " schema_id, schema_issuer_id, schema_name, schema_version," + " issuer_id, cred_def_id, and/or attr::::value where" " represents a credential attribute name" ) }, @@ -113,7 +113,7 @@ class AnoncredsPresentationReqAttrSpecSchema(OpenAPISchema): fields.Dict( keys=fields.Str( validate=validate.Regexp( - "^schema_id|schema_issuer_did|schema_name|schema_version|issuer_did|" + "^schema_id|schema_issuer_id|schema_name|schema_version|issuer_id|" "cred_def_id|attr::.+::value$" ), metadata={"example": "cred_def_id"}, @@ -124,8 +124,8 @@ class AnoncredsPresentationReqAttrSpecSchema(OpenAPISchema): 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" + " schema_id, schema_issuer_id, schema_name, schema_version," + " issuer_id, cred_def_id, and/or attr::::value where" " represents a credential attribute name" ) }, diff --git a/acapy_agent/anoncreds/registry.py b/acapy_agent/anoncreds/registry.py index c355b447cd..ff82c0d818 100644 --- a/acapy_agent/anoncreds/registry.py +++ b/acapy_agent/anoncreds/registry.py @@ -99,6 +99,11 @@ async def get_credential_definition( credential_definition_id, ) + async def get_schema_info_by_id(self, schema_id: str) -> dict: + """Get a schema info from the registry.""" + resolver = await self._resolver_for_identifier(schema_id) + return await resolver.get_schema_info_by_id(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/tests/test_manager_anoncreds.py b/acapy_agent/protocols/present_proof/v2_0/tests/test_manager_anoncreds.py index 9f88377968..8fd0db66fc 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 @@ -2078,7 +2078,7 @@ async def test_receive_pres_bait_and_switch_pred(self): "name": "highScore", "p_type": ">=", "p_value": 1000000, - "restrictions": [{"issuer_did": "FFFFFFFFFFFFFFFFFFFFFF"}], # fake issuer + "restrictions": [{"issuer_id": "FFFFFFFFFFFFFFFFFFFFFF"}], # fake issuer "non_revoked": {"from": NOW, "to": NOW}, } pres_proposal = V20PresProposal(