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

feat: Integrate AnonCreds with W3C VCDI Format Support in ACA-Py #2861

Merged
merged 23 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c3d2ee5
Add support for vc_di credential issue
tra371 Mar 29, 2024
833ca79
Copy code & cleanup
sarthakvijayvergiya Mar 30, 2024
9b275cf
Fixed test_routes.py
sarthakvijayvergiya Mar 30, 2024
4d2fa96
Update vc_di test cases
sarthakvijayvergiya Mar 31, 2024
cd599f4
fix linting
sarthakvijayvergiya Mar 31, 2024
af1ed2b
linting & code formatting
sarthakvijayvergiya Mar 31, 2024
faf3cdd
linting
sarthakvijayvergiya Mar 31, 2024
a32f225
fix test_create_credential_vcdi
sarthakvijayvergiya Mar 31, 2024
9ded95b
fix store cred
sarthakvijayvergiya Apr 1, 2024
53c24eb
Fix issuance date & proof web request
supersonicwisd1 Apr 2, 2024
6a9ee84
resolve comments & restructure vc_di models
sarthakvijayvergiya Apr 3, 2024
c911dfc
update todo comments
sarthakvijayvergiya Apr 3, 2024
74a0c0b
Allow option to switch cred type in demo and integration tests
ianco Apr 4, 2024
48bd2fb
Merge pull request #35 from ianco/feat/w3c-implementation
sarthakvijayvergiya Apr 5, 2024
e916bf1
Fix import & added proof legacy request
sarthakvijayvergiya Apr 5, 2024
ffd3552
Merge branch 'main' into feat/vc-di
sarthakvijayvergiya Apr 26, 2024
7f1f202
Merge branch 'main' into feat/vc-di
sarthakvijayvergiya Apr 27, 2024
8198f47
fix comments
sarthakvijayvergiya Apr 29, 2024
8adbea8
Merge branch 'main' into feat/vc-di
ianco May 3, 2024
0e984e6
fix entropy validation
sarthakvijayvergiya May 4, 2024
d0778a2
fix test
sarthakvijayvergiya May 4, 2024
d758e98
fix vc_di-issued credential proof request
sarthakvijayvergiya May 7, 2024
6704a2f
Merge branch 'main' into feat/vc-di
ianco May 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Contributor

@jamshale jamshale May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused what this is doing? If it is processing a cred_w3c credential wouldn't it have, and use, a return value?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We receive a W3C credential, but then we have to convert to an Indy credential to store. So, we have to process the W3C credential before we can convert to Indy format, but we don't store the W3C version of the credential explicitely.

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
Loading