Skip to content

Commit

Permalink
Merge pull request #2861 from Whats-Cookin/feat/vc-di
Browse files Browse the repository at this point in the history
feat: Integrate AnonCreds with W3C VCDI Format Support in ACA-Py
  • Loading branch information
ianco authored May 7, 2024
2 parents 8de2326 + 6704a2f commit cc3529b
Show file tree
Hide file tree
Showing 24 changed files with 2,374 additions and 33 deletions.
77 changes: 77 additions & 0 deletions aries_cloudagent/anoncreds/holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Presentation,
PresentCredentials,
create_link_secret,
W3cCredential,
)
from aries_askar import AskarError, AskarErrorCode

Expand Down Expand Up @@ -205,6 +206,25 @@ async def store_credential(
except AnoncredsError as err:
raise AnonCredsHolderError("Error processing received credential") from err

return await self._finish_store_credential(
credential_definition,
cred_recvd,
credential_request_metadata,
credential_attr_mime_types,
credential_id,
rev_reg_def,
)

async def _finish_store_credential(
self,
credential_definition: dict,
cred_recvd: Credential,
credential_request_metadata: dict,
credential_attr_mime_types: dict = None,
credential_id: str = None,
rev_reg_def: 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:
Expand Down Expand Up @@ -259,6 +279,63 @@ async def store_credential(

return credential_id

async def store_credential_w3c(
self,
credential_definition: dict,
credential_data: dict,
credential_request_metadata: dict,
credential_attr_mime_types: dict = None,
credential_id: str = None,
rev_reg_def: dict = None,
) -> str:
"""Store a credential in the wallet.
Args:
credential_definition: Credential definition for this credential
credential_data: Credential data generated by the issuer
credential_request_metadata: credential request metadata generated
by the issuer
credential_attr_mime_types: dict mapping attribute names to (optional)
MIME types to store as non-secret record, if specified
credential_id: optionally override the stored credential id
rev_reg_def: revocation registry definition in json
Returns:
the ID of the stored credential
"""
try:
secret = await self.get_master_secret()
cred_w3c = W3cCredential.load(credential_data)
await asyncio.get_event_loop().run_in_executor(
None,
cred_w3c.process,
credential_request_metadata,
secret,
credential_definition,
rev_reg_def,
)
cred_legacy = Credential.from_w3c(cred_w3c)
cred_recvd = await asyncio.get_event_loop().run_in_executor(
None,
cred_legacy.process,
credential_request_metadata,
secret,
credential_definition,
rev_reg_def,
)
except AnoncredsError as err:
raise AnonCredsHolderError("Error processing received credential") from err

return await self._finish_store_credential(
credential_definition,
cred_recvd,
credential_request_metadata,
credential_attr_mime_types,
credential_id,
rev_reg_def,
)

async def get_credentials(self, start: int, count: int, wql: dict):
"""Get credentials stored in the wallet.
Expand Down
60 changes: 60 additions & 0 deletions aries_cloudagent/anoncreds/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
CredentialOffer,
KeyCorrectnessProof,
Schema,
W3cCredential,
)
from aries_askar import AskarError

Expand Down Expand Up @@ -642,3 +643,62 @@ async def create_credential(
raise AnonCredsIssuerError("Error creating credential") from err

return credential.to_json()

async def create_credential_w3c(
self,
credential_offer: dict,
credential_request: dict,
credential_values: dict,
) -> str:
"""Create Credential."""
anoncreds_registry = self.profile.inject(AnonCredsRegistry)
schema_id = credential_offer["schema_id"]
schema_result = await anoncreds_registry.get_schema(self.profile, schema_id)
cred_def_id = credential_offer["cred_def_id"]
schema_attributes = schema_result.schema_value.attr_names

try:
async with self.profile.session() as session:
cred_def = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id)
cred_def_private = await session.handle.fetch(
CATEGORY_CRED_DEF_PRIVATE, cred_def_id
)
except AskarError as err:
raise AnonCredsIssuerError(
"Error retrieving credential definition"
) from err

if not cred_def or not cred_def_private:
raise AnonCredsIssuerError(
"Credential definition not found for credential issuance"
)

raw_values = {}
for attribute in schema_attributes:
# Ensure every attribute present in schema to be set.
# Extraneous attribute names are ignored.
try:
credential_value = credential_values[attribute]
except KeyError:
raise AnonCredsIssuerError(
"Provided credential values are missing a value "
f"for the schema attribute '{attribute}'"
)

raw_values[attribute] = str(credential_value)

try:
credential = await asyncio.get_event_loop().run_in_executor(
None,
lambda: W3cCredential.create(
cred_def.raw_value,
cred_def_private.raw_value,
credential_offer,
credential_request,
raw_values,
),
)
except AnoncredsError as err:
raise AnonCredsIssuerError("Error creating credential") from err

return credential.to_json()
5 changes: 4 additions & 1 deletion aries_cloudagent/anoncreds/tests/test_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
)
from aries_askar import AskarError, AskarErrorCode

from aries_cloudagent.anoncreds.holder import AnonCredsHolder, AnonCredsHolderError
from ..holder import AnonCredsHolder, AnonCredsHolderError
from aries_cloudagent.anoncreds.tests.mock_objects import (
MOCK_CRED,
MOCK_CRED_DEF,
Expand Down Expand Up @@ -57,6 +57,9 @@ def __init__(self, bad_schema=False, bad_cred_def=False):
def to_json_buffer(self):
return b"credential"

def to_dict(self):
return MOCK_CRED


class MockCredential:
def __init__(self, bad_schema=False, bad_cred_def=False):
Expand Down
41 changes: 36 additions & 5 deletions aries_cloudagent/anoncreds/tests/test_issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
from unittest import IsolatedAsyncioTestCase

import pytest
from anoncreds import (
Credential,
CredentialDefinition,
CredentialOffer,
)
from anoncreds import Credential, CredentialDefinition, CredentialOffer, W3cCredential
from aries_askar import AskarError, AskarErrorCode

from aries_cloudagent.anoncreds.base import (
Expand Down Expand Up @@ -744,6 +740,21 @@ async def test_create_credential_offer_create(
assert mock_create.called
assert result is not None

@mock.patch.object(InMemoryProfileSession, "handle")
@mock.patch.object(CredentialDefinition, "load", return_value=MockCredDefEntry())
@mock.patch.object(CredentialOffer, "create", return_value=MockCredOffer())
async def test_create_credential_offer_create_vcdi(
self, mock_create, mock_load, mock_session_handle
):
mock_session_handle.fetch = mock.CoroutineMock(
side_effect=[MockCredDefEntry(), MockKeyProof()]
)
result = await self.issuer.create_credential_offer("cred-def-id")
assert mock_session_handle.fetch.called
assert mock_load.called
assert mock_create.called
assert result is not None

@mock.patch.object(InMemoryProfileSession, "handle")
@mock.patch.object(Credential, "create", return_value=MockCredential())
async def test_create_credential(self, mock_create, mock_session_handle):
Expand All @@ -762,3 +773,23 @@ async def test_create_credential(self, mock_create, mock_session_handle):
assert result is not None
assert mock_session_handle.fetch.called
assert mock_create.called

@mock.patch.object(InMemoryProfileSession, "handle")
@mock.patch.object(W3cCredential, "create", return_value=MockCredential())
async def test_create_credential_vcdi(self, mock_create, mock_session_handle):
self.profile.inject = mock.Mock(
return_value=mock.MagicMock(
get_schema=mock.CoroutineMock(return_value=MockSchemaResult()),
)
)

mock_session_handle.fetch = mock.CoroutineMock(return_value=MockCredDefEntry())
result = await self.issuer.create_credential_w3c(
{"schema_id": "schema-id", "cred_def_id": "cred-def-id"},
{},
{"attr1": "value1", "attr2": "value2"},
)

assert result is not None
assert mock_session_handle.fetch.called
assert mock_create.called
11 changes: 9 additions & 2 deletions aries_cloudagent/core/in_memory/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,17 @@ def test_profile(

@classmethod
def test_session(
cls, settings: Mapping[str, Any] = None, bind: Mapping[Type, Any] = None
cls,
settings: Mapping[str, Any] = None,
bind: Mapping[Type, Any] = None,
profile_class: Any = None,
) -> "InMemoryProfileSession":
"""Used in tests to quickly create InMemoryProfileSession."""
session = InMemoryProfileSession(cls.test_profile(), settings=settings)
if profile_class is not None:
test_profile = cls.test_profile(profile_class=profile_class)
else:
test_profile = cls.test_profile()
session = InMemoryProfileSession(test_profile, settings=settings)
session._active = True
session._init_context()
if bind:
Expand Down
Empty file.
Loading

0 comments on commit cc3529b

Please sign in to comment.