Skip to content

Commit 818d8e0

Browse files
authored
Merge pull request openwallet-foundation#1783 from ianco/feature/revoc_updates
Additional endpoints to get revocation details and fix "published" status
2 parents e7fd7e5 + a2a5db0 commit 818d8e0

18 files changed

Lines changed: 827 additions & 18 deletions

aries_cloudagent/indy/credx/issuer.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,15 @@ async def revoke_credentials(
480480
await txn.handle.replace(
481481
CATEGORY_REV_REG_INFO, revoc_reg_id, value_json=rev_info
482482
)
483+
for cred_rev_id in rev_crids:
484+
issuer_cr_rec = await IssuerCredRevRecord.retrieve_by_ids(
485+
txn,
486+
revoc_reg_id,
487+
str(cred_rev_id),
488+
)
489+
await issuer_cr_rec.set_state(
490+
txn, IssuerCredRevRecord.STATE_REVOKED
491+
)
483492
if not transaction:
484493
await txn.commit()
485494
except AskarError as err:

aries_cloudagent/ledger/routes.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Ledger admin routes."""
22

33
import json
4+
import logging
45

56
from aiohttp import web
67
from aiohttp_apispec import docs, querystring_schema, request_schema, response_schema
@@ -53,6 +54,9 @@
5354
from .util import notify_register_did_event
5455

5556

57+
LOGGER = logging.getLogger(__name__)
58+
59+
5660
class LedgerModulesResultSchema(OpenAPISchema):
5761
"""Schema for the modules endpoint."""
5862

@@ -590,20 +594,35 @@ async def ledger_accept_taa(request: web.BaseRequest):
590594
raise web.HTTPForbidden(reason=reason)
591595

592596
accept_input = await request.json()
597+
LOGGER.info(">>> accepting TAA with: %s", accept_input)
593598
async with ledger:
594599
try:
595600
taa_info = await ledger.get_txn_author_agreement()
596601
if not taa_info["taa_required"]:
597602
raise web.HTTPBadRequest(
598603
reason=f"Ledger {ledger.pool_name} TAA not available"
599604
)
605+
LOGGER.info("TAA on ledger: ", taa_info)
606+
# this is a bit of a hack, but the "\ufeff" code is included in the
607+
# ledger TAA and digest calculation, so it needs to be included in the
608+
# TAA text that the user is accepting
609+
# (if you copy the TAA text using swagger it won't include this character)
610+
if taa_info["taa_record"]["text"].startswith("\ufeff"):
611+
if not accept_input["text"].startswith("\ufeff"):
612+
LOGGER.info(
613+
">>> pre-pending -endian character to TAA acceptance text"
614+
)
615+
accept_input["text"] = "\ufeff" + accept_input["text"]
600616
taa_record = {
601617
"version": accept_input["version"],
602618
"text": accept_input["text"],
603619
"digest": ledger.taa_digest(
604-
accept_input["version"], accept_input["text"]
620+
accept_input["version"],
621+
accept_input["text"],
605622
),
606623
}
624+
taa_record_digest = taa_record["digest"]
625+
LOGGER.info(">>> accepting with digest: %s", taa_record_digest)
607626
await ledger.accept_txn_author_agreement(
608627
taa_record, accept_input["mechanism"]
609628
)

aries_cloudagent/ledger/tests/test_routes.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,10 @@ async def test_accept_taa(self):
686686
with async_mock.patch.object(
687687
test_module.web, "json_response", async_mock.Mock()
688688
) as json_response:
689-
self.ledger.get_txn_author_agreement.return_value = {"taa_required": True}
689+
self.ledger.get_txn_author_agreement.return_value = {
690+
"taa_record": {"text": "text"},
691+
"taa_required": True,
692+
}
690693
result = await test_module.ledger_accept_taa(self.request)
691694
json_response.assert_called_once_with({})
692695
self.ledger.accept_txn_author_agreement.assert_awaited_once_with(
@@ -707,7 +710,10 @@ async def test_accept_taa_x(self):
707710
"mechanism": "mechanism",
708711
}
709712
)
710-
self.ledger.get_txn_author_agreement.return_value = {"taa_required": True}
713+
self.ledger.get_txn_author_agreement.return_value = {
714+
"taa_record": {"text": "text"},
715+
"taa_required": True,
716+
}
711717
self.ledger.accept_txn_author_agreement.side_effect = test_module.StorageError()
712718
with self.assertRaises(test_module.web.HTTPBadRequest):
713719
await test_module.ledger_accept_taa(self.request)

aries_cloudagent/revocation/indy.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Sequence
44

55
from ..core.profile import Profile
6+
from ..ledger.base import BaseLedger
67
from ..ledger.multiple_ledger.ledger_requests_executor import (
78
GET_CRED_DEF,
89
GET_REVOC_REG_DEF,
@@ -108,11 +109,42 @@ async def list_issuer_registries(self) -> Sequence["IssuerRevRegRecord"]:
108109
async with self._profile.session() as session:
109110
return await IssuerRevRegRecord.query(session)
110111

112+
async def get_issuer_rev_reg_delta(
113+
self, rev_reg_id: str, fro: int = None, to: int = None
114+
) -> dict:
115+
"""
116+
Check ledger for revocation status for a given revocation registry.
117+
118+
Args:
119+
rev_reg_id: ID of the revocation registry
120+
121+
"""
122+
ledger = await self.get_ledger_for_registry(rev_reg_id)
123+
async with ledger:
124+
(rev_reg_delta, _) = await ledger.get_revoc_reg_delta(
125+
rev_reg_id,
126+
fro,
127+
to,
128+
)
129+
130+
return rev_reg_delta
131+
111132
async def get_ledger_registry(self, revoc_reg_id: str) -> "RevocationRegistry":
112133
"""Get a revocation registry from the ledger, fetching as necessary."""
113134
if revoc_reg_id in IndyRevocation.REV_REG_CACHE:
114135
return IndyRevocation.REV_REG_CACHE[revoc_reg_id]
115136

137+
ledger = await self.get_ledger_for_registry(revoc_reg_id)
138+
139+
async with ledger:
140+
rev_reg = RevocationRegistry.from_definition(
141+
await ledger.get_revoc_reg_def(revoc_reg_id), True
142+
)
143+
IndyRevocation.REV_REG_CACHE[revoc_reg_id] = rev_reg
144+
return rev_reg
145+
146+
async def get_ledger_for_registry(self, revoc_reg_id: str) -> "BaseLedger":
147+
"""Get the ledger for the given registry."""
116148
multitenant_mgr = self._profile.inject_or(BaseMultitenantManager)
117149
if multitenant_mgr:
118150
ledger_exec_inst = IndyLedgerRequestsExecutor(self._profile)
@@ -124,9 +156,4 @@ async def get_ledger_registry(self, revoc_reg_id: str) -> "RevocationRegistry":
124156
txn_record_type=GET_REVOC_REG_DEF,
125157
)
126158
)[1]
127-
async with ledger:
128-
rev_reg = RevocationRegistry.from_definition(
129-
await ledger.get_revoc_reg_def(revoc_reg_id), True
130-
)
131-
IndyRevocation.REV_REG_CACHE[revoc_reg_id] = rev_reg
132-
return rev_reg
159+
return ledger
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Recover a revocation registry."""
2+
3+
import hashlib
4+
import importlib
5+
import logging
6+
import tempfile
7+
import time
8+
9+
import aiohttp
10+
import base58
11+
12+
13+
LOGGER = logging.getLogger(__name__)
14+
15+
16+
"""
17+
This module calculates a new ledger accumulator, based on the revocation status
18+
on the ledger vs revocations recorded in the wallet.
19+
The calculated transaction can be written to the ledger to get the ledger back
20+
in sync with the wallet.
21+
This function can be used if there were previous revocation errors (i.e. the
22+
credential revocation was successfully written to the wallet but the ledger write
23+
failed.)
24+
"""
25+
26+
27+
class RevocRecoveryException(Exception):
28+
"""Raise exception generating the recovery transaction."""
29+
30+
31+
async def fetch_txns(genesis_txns, registry_id):
32+
"""Fetch tails file and revocation registry information."""
33+
34+
try:
35+
vdr_module = importlib.import_module("indy_vdr")
36+
credx_module = importlib.import_module("indy_credx")
37+
except Exception as e:
38+
raise RevocRecoveryException(f"Failed to import library {e}")
39+
40+
pool = await vdr_module.open_pool(transactions=genesis_txns)
41+
LOGGER.debug("Connected to pool")
42+
43+
LOGGER.debug("Fetch registry: %s", registry_id)
44+
fetch = vdr_module.ledger.build_get_revoc_reg_def_request(None, registry_id)
45+
result = await pool.submit_request(fetch)
46+
if not result["data"]:
47+
raise RevocRecoveryException(f"Registry definition not found for {registry_id}")
48+
data = result["data"]
49+
data["ver"] = "1.0"
50+
defn = credx_module.RevocationRegistryDefinition.load(data)
51+
LOGGER.debug("Tails URL: %s", defn.tails_location)
52+
53+
async with aiohttp.ClientSession() as session:
54+
data = await session.get(defn.tails_location)
55+
tails_data = await data.read()
56+
tails_hash = base58.b58encode(hashlib.sha256(tails_data).digest()).decode(
57+
"utf-8"
58+
)
59+
if tails_hash != defn.tails_hash:
60+
raise RevocRecoveryException(
61+
f"Tails hash mismatch {tails_hash} {defn.tails_hash}"
62+
)
63+
else:
64+
LOGGER.debug("Checked tails hash: %s", tails_hash)
65+
tails_temp = tempfile.NamedTemporaryFile(delete=False)
66+
tails_temp.write(tails_data)
67+
tails_temp.close()
68+
69+
to_timestamp = int(time.time())
70+
fetch = vdr_module.ledger.build_get_revoc_reg_delta_request(
71+
None, registry_id, None, to_timestamp
72+
)
73+
result = await pool.submit_request(fetch)
74+
if not result["data"]:
75+
raise RevocRecoveryException("Error fetching delta from ledger")
76+
77+
accum_to = result["data"]["value"]["accum_to"]
78+
accum_to["ver"] = "1.0"
79+
delta = credx_module.RevocationRegistryDelta.load(accum_to)
80+
registry = credx_module.RevocationRegistry.load(accum_to)
81+
LOGGER.debug("Ledger registry state: %s", registry.to_json())
82+
revoked = set(result["data"]["value"]["revoked"])
83+
LOGGER.debug("Ledger revoked indexes: %s", revoked)
84+
85+
return defn, registry, delta, revoked, tails_temp
86+
87+
88+
async def generate_ledger_rrrecovery_txn(genesis_txns, registry_id, set_revoked):
89+
"""Generate a new ledger accum entry, based on wallet vs ledger revocation state."""
90+
91+
new_delta = None
92+
93+
ledger_data = await fetch_txns(genesis_txns, registry_id)
94+
if not ledger_data:
95+
return new_delta
96+
defn, registry, delta, prev_revoked, tails_temp = ledger_data
97+
98+
set_revoked = set(set_revoked)
99+
mismatch = prev_revoked - set_revoked
100+
if mismatch:
101+
LOGGER.warn(
102+
"Credential index(es) revoked on the ledger, but not in wallet: %s",
103+
mismatch,
104+
)
105+
106+
updates = set_revoked - prev_revoked
107+
if not updates:
108+
LOGGER.debug("No updates to perform")
109+
else:
110+
LOGGER.debug("New revoked indexes: %s", updates)
111+
112+
LOGGER.debug("tails_temp: %s", tails_temp.name)
113+
update_registry = registry.copy()
114+
new_delta = update_registry.update(defn, [], updates, tails_temp.name)
115+
116+
LOGGER.debug("New delta:")
117+
LOGGER.debug(new_delta.to_json())
118+
119+
return new_delta

0 commit comments

Comments
 (0)