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 /rotate to revocation api. #2523

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions aries_cloudagent/anoncreds/models/anoncreds_revocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ class RevRegDefState(BaseModel):
STATE_FAILED = "failed"
STATE_ACTION = "action"
STATE_WAIT = "wait"
STATE_DECOMMISSIONED = "decommissioned"
STATE_FULL = "full"

class Meta:
"""RevRegDefState metadata."""
Expand Down Expand Up @@ -179,6 +181,8 @@ class Meta:
RevRegDefState.STATE_FAILED,
RevRegDefState.STATE_ACTION,
RevRegDefState.STATE_WAIT,
RevRegDefState.STATE_DECOMMISSIONED,
RevRegDefState.STATE_FULL,
]
)
)
Expand Down
134 changes: 127 additions & 7 deletions aries_cloudagent/anoncreds/revocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import time
from typing import List, NamedTuple, Optional, Sequence, Tuple
from urllib.parse import urlparse
from uuid import uuid4

from aries_askar.error import AskarError
import base58
Expand Down Expand Up @@ -671,7 +672,121 @@ async def get_or_fetch_local_tails_path(self, rev_reg_def: RevRegDef) -> str:

async def handle_full_registry(self, rev_reg_def_id: str):
"""Update the registry status and start the next registry generation."""
# TODO
async with self.profile.session() as session:
active_rev_reg_def = await session.handle.fetch(
CATEGORY_REV_REG_DEF, rev_reg_def_id
)
if active_rev_reg_def:
# ok, we have an active rev reg.
# find the backup/fallover rev reg (finished and not active)
rev_reg_defs = await session.handle.fetch_all(
CATEGORY_REV_REG_DEF,
{
"active": json.dumps(False),
"cred_def_id": active_rev_reg_def.value_json["credDefId"],
"state": RevRegDefState.STATE_FINISHED,
},
limit=1,
)
if len(rev_reg_defs):
backup_rev_reg_def_id = rev_reg_defs[0].name
else:
# attempted to create and register here but fails in practical usage.
# the indexes and list do not get set properly (timing issue?)
# if max cred num = 4 for instance, will get
# Revocation status list does not have the index 4
# in _create_credential calling Credential.create
raise AnonCredsRevocationError(
"Error handling full registry. No backup registry available."
)

# set the backup to active...
if backup_rev_reg_def_id:
await self.set_active_registry(backup_rev_reg_def_id)

async with self.profile.transaction() as txn:
# re-fetch the old active (it's been updated), we need to mark as full
active_rev_reg_def = await txn.handle.fetch(
CATEGORY_REV_REG_DEF, rev_reg_def_id, for_update=True
)
tags = active_rev_reg_def.tags
tags["state"] = RevRegDefState.STATE_FULL
await txn.handle.replace(
CATEGORY_REV_REG_DEF,
active_rev_reg_def.name,
active_rev_reg_def.value,
tags,
)
await txn.commit()

# create our next fallover/backup
backup_reg = await self.create_and_register_revocation_registry_definition(
issuer_id=active_rev_reg_def.value_json["issuerId"],
cred_def_id=active_rev_reg_def.value_json["credDefId"],
registry_type=active_rev_reg_def.value_json["revocDefType"],
tag=str(uuid4()),
max_cred_num=active_rev_reg_def.value_json["value"]["maxCredNum"],
)
LOGGER.info(f"previous rev_reg_def_id = {rev_reg_def_id}")
LOGGER.info(f"current rev_reg_def_id = {backup_rev_reg_def_id}")
LOGGER.info(f"backup reg = {backup_reg}")

async def decommission_registry(self, cred_def_id: str):
"""Decommission post-init registries and start the next registry generation."""
active_reg = await self.get_or_create_active_registry(cred_def_id)

# create new one and set active
new_reg = await self.create_and_register_revocation_registry_definition(
issuer_id=active_reg.rev_reg_def.issuer_id,
cred_def_id=active_reg.rev_reg_def.cred_def_id,
registry_type=active_reg.rev_reg_def.type,
tag=str(uuid4()),
max_cred_num=active_reg.rev_reg_def.value.max_cred_num,
)
# set new as active...
await self.set_active_registry(new_reg.rev_reg_def_id)

# decommission everything except init/wait
async with self.profile.transaction() as txn:
registries = await txn.handle.fetch_all(
CATEGORY_REV_REG_DEF,
{
"cred_def_id": cred_def_id,
},
for_update=True,
)

recs = list(
filter(
lambda r: r.tags.get("state") != RevRegDefState.STATE_WAIT,
registries,
)
)
for rec in recs:
if rec.name != new_reg.rev_reg_def_id:
tags = rec.tags
tags["active"] = json.dumps(False)
tags["state"] = RevRegDefState.STATE_DECOMMISSIONED
await txn.handle.replace(
CATEGORY_REV_REG_DEF,
rec.name,
rec.value,
tags,
)
await txn.commit()
# create a second one for backup, don't make it active
backup_reg = await self.create_and_register_revocation_registry_definition(
issuer_id=active_reg.rev_reg_def.issuer_id,
cred_def_id=active_reg.rev_reg_def.cred_def_id,
registry_type=active_reg.rev_reg_def.type,
tag=str(uuid4()),
max_cred_num=active_reg.rev_reg_def.value.max_cred_num,
)

LOGGER.info(f"new reg = {new_reg}")
LOGGER.info(f"backup reg = {backup_reg}")
LOGGER.info(f"decommissioned regs = {recs}")
return recs

async def get_or_create_active_registry(self, cred_def_id: str) -> RevRegDefResult:
"""Get or create a revocation registry for the given cred def id."""
Expand Down Expand Up @@ -801,6 +916,8 @@ async def _create_credential(
"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,
Expand Down Expand Up @@ -903,12 +1020,15 @@ async def create_credential(
# unlucky, another instance filled the registry first
continue

if (
rev_reg_def_result
and rev_reg_def_result.rev_reg_def.value.max_cred_num
<= int(cred_rev_id)
):
await self.handle_full_registry(rev_reg_def_id)
# cred rev id is zero based
# max cred num is one based
# however, if we wait until max cred num is reached, we are too late.
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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq
options = {}
if support_revocation:
options["support_revocation"] = True
options["revocation_registry_size"] = rev_reg_size
options["max_cred_num"] = rev_reg_size
if create_transaction_for_endorser:
endorser_connection_id = await get_endorser_connection_id(context.profile)
if not endorser_connection_id:
Expand Down
70 changes: 68 additions & 2 deletions aries_cloudagent/revocation/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from marshmallow import fields, validate, validates_schema
from marshmallow.exceptions import ValidationError

from ..anoncreds.models.anoncreds_revocation import RevRegDefState

from ..anoncreds.default.legacy_indy.registry import LegacyIndyRegistry
from ..anoncreds.base import (
AnonCredsObjectNotFound,
Expand Down Expand Up @@ -351,8 +353,8 @@ class RevRegsCreatedQueryStringSchema(OpenAPISchema):
required=False,
validate=validate.OneOf(
[
getattr(IssuerRevRegRecord, m)
for m in vars(IssuerRevRegRecord)
getattr(RevRegDefState, m)
for m in vars(RevRegDefState)
if m.startswith("STATE_")
]
),
Expand Down Expand Up @@ -624,6 +626,61 @@ async def _get_issuer_rev_reg_record(
return result


@docs(
tags=["revocation"],
summary="Get current active revocation registry by credential definition id",
)
@match_info_schema(RevocationCredDefIdMatchInfoSchema())
@response_schema(RevRegResultSchema(), 200, description="")
async def get_active_rev_reg(request: web.BaseRequest):
"""Request handler to get current active revocation registry by cred def id.

Args:
request: aiohttp request object

Returns:
The revocation registry identifier

"""
context: AdminRequestContext = request["context"]
profile: AskarProfile = context.profile
cred_def_id = request.match_info["cred_def_id"]
try:
revocation = AnonCredsRevocation(profile)
active_reg = await revocation.get_or_create_active_registry(cred_def_id)
rev_reg = await _get_issuer_rev_reg_record(profile, active_reg.rev_reg_def_id)
except AnonCredsIssuerError as e:
raise web.HTTPInternalServerError(reason=str(e)) from e

return web.json_response({"result": rev_reg.serialize()})


@docs(tags=["revocation"], summary="Rotate revocation registry")
@match_info_schema(RevocationCredDefIdMatchInfoSchema())
@response_schema(RevRegsCreatedSchema(), 200, description="")
async def rotate_rev_reg(request: web.BaseRequest):
"""Request handler to rotate the active revocation registries for cred. def.

Args:
request: aiohttp request object

Returns:
list or revocation registry ids that were rotated out

"""
context: AdminRequestContext = request["context"]
profile: AskarProfile = context.profile
cred_def_id = request.match_info["cred_def_id"]

try:
revocation = AnonCredsRevocation(profile)
recs = await revocation.decommission_registry(cred_def_id)
except AnonCredsIssuerError as e:
raise web.HTTPInternalServerError(reason=str(e)) from e

return web.json_response({"rev_reg_ids": [rec.name for rec in recs if rec.name]})


@docs(
tags=["revocation"],
summary="Get number of credentials issued against revocation registry",
Expand Down Expand Up @@ -966,6 +1023,15 @@ async def register(app: web.Application):
allow_head=False,
),
web.get("/revocation/registry/{rev_reg_id}", get_rev_reg, allow_head=False),
web.get(
"/revocation/active-registry/{cred_def_id}",
get_active_rev_reg,
allow_head=False,
),
web.post(
"/revocation/active-registry/{cred_def_id}/rotate",
rotate_rev_reg,
),
web.get(
"/revocation/registry/{rev_reg_id}/issued",
get_rev_reg_issued_count,
Expand Down
10 changes: 8 additions & 2 deletions demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ Faber will setup support for revocation automatically, and you will see an extra
(4) Create New Invitation
(5) Revoke Credential
(6) Publish Revocations
(7) Rotate Revocation Registry
(8) List Revocation Registries
(T) Toggle tracing on credential/proof exchange
(X) Exit?
```
Expand All @@ -243,14 +245,18 @@ Faber | Credential revocation ID: 1
When you revoke a credential you will need to provide those values:

```
[1/2/3/4/5/6/T/X] 5
[1/2/3/4/5/6/7/8/T/X] 5

Enter revocation registry ID: WGmUNAdH2ZfeGvacFoMVVP:4:WGmUNAdH2ZfeGvacFoMVVP:3:CL:38:Faber.Agent.degree_schema:CL_ACCUM:15ca49ed-1250-4608-9e8f-c0d52d7260c3
Enter credential revocation ID: 1
Publish now? [Y/N]: y
```

Note that you need to Publish the revocation information to the ledger. Once you've revoked a credential any proof which uses this credential will fail to verify.
Note that you need to Publish the revocation information to the ledger. Once you've revoked a credential any proof which uses this credential will fail to verify.

Rotating the revocation registry will decommission any "ready" registry records and create 2 new registry records. You can view in the logs as the records are created and transition to 'active'. There should always be 2 'active' revocation registries - one working and one for hot-swap. Note that revocation information can still be published from decommissioned registries.

You can also list the created registries, filtering by current state: 'init', 'generated', 'posted', 'active', 'full', 'decommissioned'.

### DID Exchange

Expand Down
61 changes: 61 additions & 0 deletions demo/features/revocation-api.feature
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,64 @@ Feature: ACA-Py Revocation API
Examples:
| issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request |
| Acme | --revocation --public-did --did-exchange | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 |

@GHA
Scenario Outline: Using revocation api, rotate revocation
Given we have "3" agents
| name | role | capabilities |
| Acme | issuer | <Acme_capabilities> |
| Faber | verifier | <Acme_capabilities> |
| Bob | prover | <Bob_capabilities> |
And "<issuer>" and "Bob" have an existing connection
And "Bob" has an issued <Schema_name> credential <Credential_data> from "<issuer>"
And "<issuer>" lists revocation registries
And "<issuer>" rotates revocation registries

Examples:
| issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request |
| Acme | --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 |

@GHA
Scenario Outline: Using revocation api, fill registry (need to run with "TAILS_FILE_COUNT": "4" env var)
Given we have "2" agents
| name | role | capabilities |
| Acme | issuer | <Acme_capabilities> |
| Bob | prover | <Bob_capabilities> |
And "<issuer>" and "Bob" have an existing connection
And "Bob" has an issued <Schema_name> credential <Credential_data> from "<issuer>"
And wait 5 seconds
And "<issuer>" lists revocation registries 1
When "<issuer>" offers a credential with data <Credential_data>
Then "Bob" has the credential issued
And wait 5 seconds
And "<issuer>" lists revocation registries 2
When "<issuer>" offers a credential with data <Credential_data>
Then "Bob" has the credential issued
And wait 5 seconds
And "<issuer>" lists revocation registries 3
When "<issuer>" offers a credential with data <Credential_data>
Then "Bob" has the credential issued
And wait 5 seconds
And "<issuer>" lists revocation registries 4
When "<issuer>" offers a credential with data <Credential_data>
Then "Bob" has the credential issued
And wait 5 seconds
And "<issuer>" lists revocation registries 5
When "<issuer>" offers a credential with data <Credential_data>
Then "Bob" has the credential issued
And wait 5 seconds
And "<issuer>" lists revocation registries 6
When "<issuer>" offers a credential with data <Credential_data>
Then "Bob" has the credential issued
And wait 5 seconds
And "<issuer>" lists revocation registries 7
When "<issuer>" offers a credential with data <Credential_data>
Then "Bob" has the credential issued
And "<issuer>" lists revocation registries 8
When "<issuer>" offers a credential with data <Credential_data>
Then "Bob" has the credential issued
And wait 5 seconds

Examples:
| issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request |
| Acme | --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 |
Loading
Loading