Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anoncreds create credential #3369

Merged
merged 14 commits into from
Dec 20, 2024
9 changes: 8 additions & 1 deletion 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,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(
Expand Down
7 changes: 7 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,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)
7 changes: 7 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,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)
12 changes: 12 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,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),
)
Original file line number Diff line number Diff line change
Expand Up @@ -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"
29 changes: 11 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,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",
}

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
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
8 changes: 8 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,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,
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
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading