Skip to content

Commit

Permalink
Merge pull request #2967 from Whats-Cookin/vcdi-revocation
Browse files Browse the repository at this point in the history
Add support for revocable credentials in vc_di handler
  • Loading branch information
ianco authored Jun 10, 2024
2 parents 1da9872 + 9d01fd7 commit 85359ec
Show file tree
Hide file tree
Showing 6 changed files with 532 additions and 117 deletions.
95 changes: 86 additions & 9 deletions aries_cloudagent/anoncreds/revocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import os
import time
from pathlib import Path
from typing import List, NamedTuple, Optional, Sequence, Tuple
from typing import List, NamedTuple, Optional, Sequence, Tuple, Union
from urllib.parse import urlparse

import base58
Expand All @@ -19,6 +19,7 @@
RevocationRegistryDefinition,
RevocationRegistryDefinitionPrivate,
RevocationStatusList,
W3cCredential,
)
from aries_askar.error import AskarError
from requests import RequestException, Session
Expand Down Expand Up @@ -717,6 +718,7 @@ async def upload_tails_file(self, rev_reg_def: RevRegDef):
backoff=-0.5,
max_attempts=5, # heuristic: respect HTTP timeout
)

if not upload_success:
raise AnonCredsRevocationError(
f"Tails file for rev reg for {rev_reg_def.cred_def_id} "
Expand Down Expand Up @@ -892,16 +894,61 @@ async def get_or_create_active_registry(self, cred_def_id: str) -> RevRegDefResu

# Credential Operations

async def create_credential_w3c(
self,
w3c_credential_offer: dict,
w3c_credential_request: dict,
w3c_credential_values: dict,
*,
retries: int = 5,
) -> Tuple[str, str, str]:
"""Create a w3c_credential.
Args:
w3c_credential_offer: Credential Offer to create w3c_credential for
w3c_credential_request: Credential request to create w3c_credential for
w3c_credential_values: Values to go in w3c_credential
retries: number of times to retry w3c_credential creation
Returns:
A tuple of created w3c_credential and revocation id
"""
return await self._create_credential_helper(
w3c_credential_offer,
w3c_credential_request,
w3c_credential_values,
W3cCredential,
retries=retries,
)

async def _create_credential(
self,
credential_definition_id: str,
schema_attributes: List[str],
credential_offer: dict,
credential_request: dict,
credential_values: dict,
credential_type: Union[Credential, W3cCredential],
rev_reg_def_id: Optional[str] = None,
tails_file_path: Optional[str] = None,
) -> Tuple[str, str]:
"""Create a credential.
Args:
credential_definition_id: The credential definition ID
schema_attributes: The schema attributes
credential_offer: The credential offer
credential_request: The credential request
credential_values: The credential values
credential_type: The credential type
rev_reg_def_id: The revocation registry definition ID
tails_file_path: The tails file path
Returns:
A tuple of created credential and revocation ID
"""
try:
async with self.profile.session() as session:
cred_def = await session.handle.fetch(
Expand Down Expand Up @@ -1004,14 +1051,13 @@ async def _create_credential(
try:
credential = await asyncio.get_event_loop().run_in_executor(
None,
lambda: Credential.create(
cred_def.raw_value,
cred_def_private.raw_value,
credential_offer,
credential_request,
raw_values,
None,
revoc,
lambda: credential_type.create(
cred_def=cred_def.raw_value,
cred_def_private=cred_def_private.raw_value,
cred_offer=credential_offer,
cred_request=credential_request,
attr_raw_values=raw_values,
revocation_config=revoc,
),
)
except AnoncredsError as err:
Expand Down Expand Up @@ -1039,6 +1085,36 @@ async def create_credential(
Returns:
A tuple of created credential and revocation id
"""
return await self._create_credential_helper(
credential_offer,
credential_request,
credential_values,
Credential,
retries=retries,
)

async def _create_credential_helper(
self,
credential_offer: dict,
credential_request: dict,
credential_values: dict,
credential_type: Union[Credential, W3cCredential],
*,
retries: int = 5,
) -> Tuple[str, str, str]:
"""Create a credential.
Args:
credential_offer: Credential Offer to create credential for
credential_request: Credential request to create credential for
credential_values: Values to go in credential
credential_type: Credential or W3cCredential
retries: number of times to retry credential creation
Returns:
A tuple of created credential and revocation id
"""
issuer = AnonCredsIssuer(self.profile)
anoncreds_registry = self.profile.inject(AnonCredsRegistry)
Expand Down Expand Up @@ -1081,6 +1157,7 @@ async def create_credential(
credential_offer,
credential_request,
credential_values,
credential_type,
rev_reg_def_id,
tails_file_path,
)
Expand Down
88 changes: 88 additions & 0 deletions aries_cloudagent/anoncreds/tests/test_revocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
RevocationRegistryDefinitionPrivate,
RevocationStatusList,
Schema,
# AnoncredsError,
# W3cCredential,
# CredentialRevocationConfig,
)
from aries_askar import AskarError, AskarErrorCode
from requests import RequestException, Session
Expand Down Expand Up @@ -1024,6 +1027,7 @@ async def test_create_credential_private_no_rev_reg_or_tails(
"attr1": "value1",
"attr2": "value2",
},
credential_type=Credential,
)
assert mock_create.called

Expand All @@ -1038,6 +1042,7 @@ async def test_create_credential_private_no_rev_reg_or_tails(
credential_offer={},
credential_request={},
credential_values={},
credential_type=Credential,
)

# missing cred def or cred def private
Expand All @@ -1049,6 +1054,7 @@ async def test_create_credential_private_no_rev_reg_or_tails(
credential_offer={},
credential_request={},
credential_values={},
credential_type=Credential,
)
mock_handle.fetch = mock.CoroutineMock(side_effect=[MockEntry(), None])
with self.assertRaises(test_module.AnonCredsRevocationError):
Expand All @@ -1058,6 +1064,7 @@ async def test_create_credential_private_no_rev_reg_or_tails(
credential_offer={},
credential_request={},
credential_values={},
credential_type=Credential,
)

@mock.patch.object(InMemoryProfileSession, "handle")
Expand Down Expand Up @@ -1086,6 +1093,7 @@ async def call_test_func():
},
rev_reg_def_id="test-rev-reg-def-id",
tails_file_path="tails-file-path",
credential_type=Credential,
)

# missing rev list
Expand Down Expand Up @@ -1380,3 +1388,83 @@ async def test_clear_pending_revocations_with_non_anoncreds_session(self):
await self.revocation.clear_pending_revocations(
self.profile.session(), rev_reg_def_id="test-rev-reg-id"
)

@mock.patch.object(
AnonCredsIssuer, "cred_def_supports_revocation", return_value=True
)
async def test_create_credential_w3c(self, mock_supports_revocation):
self.profile.inject = mock.Mock(
return_value=mock.MagicMock(
get_schema=mock.CoroutineMock(
return_value=GetSchemaResult(
schema_id="CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3",
schema=AnonCredsSchema(
issuer_id="CsQY9MGeD3CQP4EyuVFo5m",
name="MYCO Biomarker:0.0.3",
version="1.0",
attr_names=["attr1", "attr2"],
),
schema_metadata={},
resolution_metadata={},
)
)
)
)
self.revocation.get_or_create_active_registry = mock.CoroutineMock(
return_value=RevRegDefResult(
job_id="test-job-id",
revocation_registry_definition_state=RevRegDefState(
state=RevRegDefState.STATE_FINISHED,
revocation_registry_definition_id="active-reg-reg",
revocation_registry_definition=rev_reg_def,
),
registration_metadata={},
revocation_registry_definition_metadata={},
)
)

# Test private funtion seperately - very large
self.revocation._create_credential = mock.CoroutineMock(
return_value=({"cred": "cred"}, 98)
)

result = await self.revocation.create_credential_w3c(
w3c_credential_offer={
"schema_id": "CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3",
"cred_def_id": "CsQY9MGeD3CQP4EyuVFo5m:3:CL:14951:MYCO_Biomarker",
"key_correctness_proof": {},
"nonce": "nonce",
},
w3c_credential_request={},
w3c_credential_values={},
)

assert isinstance(result, tuple)
assert mock_supports_revocation.call_count == 1

@pytest.mark.asyncio
@mock.patch.object(InMemoryProfileSession, "handle")
async def test_create_credential_w3c_keyerror(self, mock_handle):
mock_handle.fetch = mock.CoroutineMock(side_effect=[MockEntry(), MockEntry()])
with pytest.raises(test_module.AnonCredsRevocationError) as excinfo:
await self.revocation._create_credential(
credential_definition_id="test-cred-def-id",
schema_attributes=["attr1", "attr2"],
credential_offer={
"schema_id": "CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3",
"cred_def_id": "CsQY9MGeD3CQP4EyuVFo5m:3:CL:14951:MYCO_Biomarker",
"key_correctness_proof": {},
"nonce": "nonce",
},
credential_request={},
credential_values={
"X": "value1",
"Y": "value2",
},
credential_type=Credential,
)

assert str(excinfo.value) == (
"Provided credential values are missing a value "
"for the schema attribute 'attr1'"
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import json
import logging
from typing import Mapping, Tuple

from anoncreds import W3cCredential
from ...models.cred_ex_record import V20CredExRecord
from ...models.detail.indy import (
V20CredExRecordIndy,
Expand Down Expand Up @@ -460,25 +460,36 @@ async def issue_credential(

async with ledger:
schema_id = await ledger.credential_definition_id2schema_id(cred_def_id)
cred_def = await ledger.get_credential_definition(cred_def_id)
revocable = cred_def["value"].get("revocation")

legacy_offer = await self._prepare_legacy_offer(cred_offer, schema_id)
legacy_request = await self._prepare_legacy_request(cred_request, cred_def_id)

issuer = AnonCredsIssuer(self.profile)

credential = await issuer.create_credential_w3c(
legacy_offer, legacy_request, cred_values
)
cred_rev_id = None
rev_reg_def_id = None
credential = None
if revocable:
issuer = AnonCredsRevocation(self.profile)
(
credential,
cred_rev_id,
rev_reg_def_id,
) = await issuer.create_credential_w3c(
legacy_offer, legacy_request, cred_values
)
else:
issuer = AnonCredsIssuer(self.profile)
credential = await issuer.create_credential_w3c(
legacy_offer, legacy_request, cred_values
)

vcdi_credential = {
"credential": json.loads(credential),
}

result = self.get_format_data(CRED_20_ISSUE, vcdi_credential)

cred_rev_id = None
rev_reg_def_id = None

async with self._profile.transaction() as txn:
detail_record = V20CredExRecordIndy(
cred_ex_id=cred_ex_record.cred_ex_id,
Expand Down Expand Up @@ -554,10 +565,18 @@ async def store_credential(
cred_def_result = await anoncreds_registry.get_credential_definition(
self.profile, cred["proof"][0]["verificationMethod"]
)
if cred["proof"][0].get("rev_reg_id"):
# TODO: remove loading of W3cCredential and use the credential directly
try:
cred_w3c = W3cCredential.load(cred)
rev_reg_id = cred_w3c.rev_reg_id
rev_reg_index = cred_w3c.rev_reg_index
except AnonCredsHolderError as e:
LOGGER.error(f"Error receiving credential: {e.error_code} - {e.message}")
raise e
if rev_reg_id:
rev_reg_def_result = (
await anoncreds_registry.get_revocation_registry_definition(
self.profile, cred["proof"][0]["rev_reg_id"]
self.profile, rev_reg_id
)
)
rev_reg_def = rev_reg_def_result.revocation_registry
Expand Down Expand Up @@ -588,8 +607,8 @@ async def store_credential(
)

detail_record.cred_id_stored = cred_id_stored
detail_record.rev_reg_id = cred["proof"][0].get("rev_reg_id", None)
detail_record.cred_rev_id = cred["proof"][0].get("cred_rev_id", None)
detail_record.rev_reg_id = rev_reg_id
detail_record.cred_rev_id = rev_reg_index

async with self.profile.session() as session:
# Store detail record, emit event
Expand Down
Loading

0 comments on commit 85359ec

Please sign in to comment.