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

Add support for revocable credentials in vc_di handler #2967

Merged
merged 11 commits into from
Jun 10, 2024
203 changes: 203 additions & 0 deletions aries_cloudagent/anoncreds/revocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@
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,6 +894,207 @@

# Credential Operations

async def _create_credential_w3c(
self,
w3c_credential_definition_id: str,
schema_attributes: List[str],
w3c_credential_offer: dict,
w3c_credential_request: dict,
w3c_credential_values: dict,
rev_reg_def_id: Optional[str] = None,
tails_file_path: Optional[str] = None,
) -> Tuple[str, str]:
try:
async with self.profile.session() as session:
cred_def = await session.handle.fetch(
CATEGORY_CRED_DEF, w3c_credential_definition_id
)
cred_def_private = await session.handle.fetch(
CATEGORY_CRED_DEF_PRIVATE, w3c_credential_definition_id
)
except AskarError as err:
raise AnonCredsRevocationError(
"Error retrieving w3c_credential definition"
) from err
if not cred_def or not cred_def_private:
raise AnonCredsRevocationError(
"Credential definition not found for w3c_credential issuance"
)

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

raw_values[attribute] = str(w3c_credential_value)

if rev_reg_def_id and tails_file_path:
try:
async with self.profile.transaction() as txn:
rev_list = await txn.handle.fetch(CATEGORY_REV_LIST, rev_reg_def_id)
rev_reg_def = await txn.handle.fetch(
CATEGORY_REV_REG_DEF, rev_reg_def_id
)
rev_key = await txn.handle.fetch(
CATEGORY_REV_REG_DEF_PRIVATE, rev_reg_def_id
)
if not rev_list:
raise AnonCredsRevocationError("Revocation registry not found")
if not rev_reg_def:
raise AnonCredsRevocationError(
"Revocation registry definition not found"
)
if not rev_key:
raise AnonCredsRevocationError(
"Revocation registry definition private data not found"
)
rev_info = rev_list.value_json
rev_info_tags = rev_list.tags
rev_reg_index = rev_info["next_index"]
try:
rev_reg_def = RevocationRegistryDefinition.load(
rev_reg_def.raw_value
)
rev_list = RevocationStatusList.load(rev_info["rev_list"])
except AnoncredsError as err:
raise AnonCredsRevocationError(
"Error loading revocation registry definition"
) from err
if rev_reg_index > rev_reg_def.max_cred_num:
raise AnonCredsRevocationRegistryFullError(
"Revocation registry is full"
)
rev_info["next_index"] = rev_reg_index + 1
await txn.handle.replace(
CATEGORY_REV_LIST,
rev_reg_def_id,
value_json=rev_info,
tags=rev_info_tags,
)
await txn.commit()
except AskarError as err:
raise AnonCredsRevocationError(
"Error updating revocation registry index"
) from err

# rev_info["next_index"] is 1 based but getting from
# rev_list is zero based...
revoc = CredentialRevocationConfig(
rev_reg_def,
rev_key.raw_value,
rev_list,
rev_reg_index,
)
w3c_credential_revocation_id = str(rev_reg_index)
else:
revoc = None
w3c_credential_revocation_id = None
rev_list = None

try:
w3c_credential = await asyncio.get_event_loop().run_in_executor(
None,
lambda: W3cCredential.create(
cred_def=cred_def.raw_value,
cred_def_private=cred_def_private.raw_value,
cred_offer=w3c_credential_offer,
cred_request=w3c_credential_request,
attr_raw_values=raw_values,
revocation_config=revoc,
),
)

except AnoncredsError as err:
raise AnonCredsRevocationError("Error creating credential") from err

return w3c_credential.to_json(), w3c_credential_revocation_id

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
revoc_reg_id: ID of the revocation registry
retries: number of times to retry w3c_credential creation

Returns:
A tuple of created w3c_credential and revocation id

"""
issuer = AnonCredsIssuer(self.profile)
anoncreds_registry = self.profile.inject(AnonCredsRegistry)
schema_id = w3c_credential_offer["schema_id"]
schema_result = await anoncreds_registry.get_schema(self.profile, schema_id)
cred_def_id = w3c_credential_offer["cred_def_id"]
schema_attributes = schema_result.schema_value.attr_names

revocable = await issuer.cred_def_supports_revocation(cred_def_id)

for attempt in range(max(retries, 1)):
if attempt > 0:
LOGGER.info(
"Retrying w3c_credential creation for revocation registry %s",
cred_def_id,

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
This expression logs
sensitive data (secret)
as clear text.
This expression logs
sensitive data (secret)
as clear text.
)
await asyncio.sleep(2)

rev_reg_def_result = None
if revocable:
rev_reg_def_result = await self.get_or_create_active_registry(
cred_def_id
)
if (
rev_reg_def_result.revocation_registry_definition_state.state
!= STATE_FINISHED
):
continue
rev_reg_def_id = rev_reg_def_result.rev_reg_def_id
tails_file_path = self.get_local_tails_path(
rev_reg_def_result.rev_reg_def
)
else:
rev_reg_def_id = None
tails_file_path = None

try:
cred_json, cred_rev_id = await self._create_credential_w3c(
cred_def_id,
schema_attributes,
w3c_credential_offer,
w3c_credential_request,
w3c_credential_values,
rev_reg_def_id,
tails_file_path,
)
except AnonCredsRevocationRegistryFullError:
continue

if rev_reg_def_result:
if (
rev_reg_def_result.rev_reg_def.value.max_cred_num
<= int(cred_rev_id) + 1
):
await self.handle_full_registry(rev_reg_def_id)
return cred_json, cred_rev_id, rev_reg_def_id

raise AnonCredsRevocationError("Failed to create w3c_credential after retrying")

async def _create_credential(
self,
credential_definition_id: str,
Expand Down
127 changes: 127 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,7 @@
RevocationRegistryDefinitionPrivate,
RevocationStatusList,
Schema,
W3cCredential,
)
from aries_askar import AskarError, AskarErrorCode
from requests import RequestException, Session
Expand Down Expand Up @@ -1380,3 +1381,129 @@ 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={},
)
)

self.revocation._create_credential_w3c = 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

@mock.patch.object(InMemoryProfileSession, "handle")
@mock.patch.object(W3cCredential, "create", return_value=mock.MagicMock())
async def test_create_credential_w3c_private_no_rev_reg_or_tails(
self, mock_create, mock_handle
):
mock_handle.fetch = mock.CoroutineMock(side_effect=[MockEntry(), MockEntry()])
await self.revocation._create_credential_w3c(
w3c_credential_definition_id="test-cred-def-id",
schema_attributes=["attr1", "attr2"],
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={
"attr1": "value1",
"attr2": "value2",
},
)
assert mock_create.called

# askar error retrieving cred def
mock_handle.fetch = mock.CoroutineMock(
side_effect=AskarError(AskarErrorCode.UNEXPECTED, "test")
)
with self.assertRaises(test_module.AnonCredsRevocationError):
await self.revocation._create_credential_w3c(
w3c_credential_definition_id="test-cred-def-id",
schema_attributes=["attr1", "attr2"],
w3c_credential_offer={},
w3c_credential_request={},
w3c_credential_values={},
)

# missing cred def or cred def private
mock_handle.fetch = mock.CoroutineMock(side_effect=[None, MockEntry()])
with self.assertRaises(test_module.AnonCredsRevocationError):
await self.revocation._create_credential_w3c(
w3c_credential_definition_id="test-cred-def-id",
schema_attributes=["attr1", "attr2"],
w3c_credential_offer={},
w3c_credential_request={},
w3c_credential_values={},
)
mock_handle.fetch = mock.CoroutineMock(side_effect=[MockEntry(), None])
with self.assertRaises(test_module.AnonCredsRevocationError):
await self.revocation._create_credential_w3c(
w3c_credential_definition_id="test-cred-def-id",
schema_attributes=["attr1", "attr2"],
w3c_credential_offer={},
w3c_credential_request={},
w3c_credential_values={},
)

mock_handle.fetch = mock.CoroutineMock(side_effect=[MockEntry(), None])
with self.assertRaises(test_module.AnonCredsRevocationError):
await self.revocation._create_credential_w3c(
w3c_credential_definition_id="test-cred-def-id",
schema_attributes=["attr1", "attr2"],
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={
"x": "value1",
"y": "value2",
},
)
Loading
Loading