Skip to content

Commit

Permalink
Fix anoncreds issuance and compatibility
Browse files Browse the repository at this point in the history
Signed-off-by: jamshale <[email protected]>
  • Loading branch information
sownak committed Dec 12, 2024
1 parent a402df0 commit fd30c67
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 64 deletions.
5 changes: 5 additions & 0 deletions acapy_agent/anoncreds/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
RevRegDefResult,
)
from .models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult
from .models.schema_info import AnoncredsSchemaInfo

T = TypeVar("T")

Expand Down Expand Up @@ -130,6 +131,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) -> AnoncredsSchemaInfo:
"""Get a schema info from the registry."""


class BaseAnonCredsRegistrar(BaseAnonCredsHandler):
"""Base Anon Creds Registrar."""
Expand Down
5 changes: 5 additions & 0 deletions acapy_agent/anoncreds/default/did_indy/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
RevRegDefResult,
)
from ...models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult
from ...models.schema_info import AnoncredsSchemaInfo

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -118,3 +119,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) -> AnoncredsSchemaInfo:
"""Get a schema info from the registry."""
return await super().get_schema_info_by_id(schema_id)
5 changes: 5 additions & 0 deletions acapy_agent/anoncreds/default/did_web/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
RevRegDefResult,
)
from ...models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult
from ...models.schema_info import AnoncredsSchemaInfo


class DIDWebRegistry(BaseAnonCredsResolver, BaseAnonCredsRegistrar):
Expand Down Expand Up @@ -113,3 +114,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) -> AnoncredsSchemaInfo:
"""Get a schema info from the registry."""
return await super().get_schema_info_by_id(schema_id)
10 changes: 10 additions & 0 deletions acapy_agent/anoncreds/default/legacy_indy/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
SchemaResult,
SchemaState,
)
from ...models.schema_info import AnoncredsSchemaInfo
from ...revocation import (
CATEGORY_REV_LIST,
CATEGORY_REV_REG_DEF,
Expand Down Expand Up @@ -1229,3 +1230,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) -> 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),
)
Original file line number Diff line number Diff line change
Expand Up @@ -1210,3 +1210,11 @@ 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(
"XduBsoPyEA4szYMy3pZ8De:2:minimal-33279d005748b3cc:1.0",
)
assert result.issuer_id == "XduBsoPyEA4szYMy3pZ8De"
assert result.name == "minimal-33279d005748b3cc"
assert result.version == "1.0"
27 changes: 9 additions & 18 deletions acapy_agent/anoncreds/holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import asyncio
import json
import logging
import re
from typing import Dict, Optional, Sequence, Tuple, Union

from anoncreds import (
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
}

Expand Down
13 changes: 12 additions & 1 deletion acapy_agent/anoncreds/models/credential_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand Down
12 changes: 6 additions & 6 deletions acapy_agent/anoncreds/models/presentation_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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::<attribute-name>::value where"
" schema_id, schema_issuer_id, schema_name, schema_version,"
" issuer_id, cred_def_id, and/or attr::<attribute-name>::value where"
" <attribute-name> represents a credential attribute name"
)
},
Expand Down Expand Up @@ -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"},
Expand All @@ -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::<attribute-name>::value where"
" schema_id, schema_issuer_id, schema_name, schema_version,"
" issuer_id, cred_def_id, and/or attr::<attribute-name>::value where"
" <attribute-name> represents a credential attribute name"
)
},
Expand Down
26 changes: 26 additions & 0 deletions acapy_agent/anoncreds/models/schema_info.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions acapy_agent/anoncreds/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
RevRegDefResult,
)
from .models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult
from .models.schema_info import AnoncredsSchemaInfo

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -99,6 +100,11 @@ async def get_credential_definition(
credential_definition_id,
)

async def get_schema_info_by_id(self, 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(schema_id)

async def register_credential_definition(
self,
profile: Profile,
Expand Down
50 changes: 13 additions & 37 deletions acapy_agent/anoncreds/tests/test_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"

Expand All @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand All @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions acapy_agent/indy/credx/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit fd30c67

Please sign in to comment.