From 273600019710421a10e24879f072ff30c349369b Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 4 May 2023 15:42:25 -0400 Subject: [PATCH] feat: add generic tails file upload Signed-off-by: Daniel Bluhm --- .../anoncreds/default/legacy_indy/registry.py | 25 +---- aries_cloudagent/anoncreds/issuer.py | 101 ++++++++++++++---- aries_cloudagent/config/default_context.py | 2 +- .../models/issuer_rev_reg_record.py | 2 +- .../tails/anoncreds_tails_server.py | 64 +++++++++++ aries_cloudagent/tails/base.py | 9 +- aries_cloudagent/tails/indy_tails_server.py | 10 +- 7 files changed, 163 insertions(+), 50 deletions(-) create mode 100644 aries_cloudagent/tails/anoncreds_tails_server.py diff --git a/aries_cloudagent/anoncreds/default/legacy_indy/registry.py b/aries_cloudagent/anoncreds/default/legacy_indy/registry.py index c1ae812bde..e2c730d736 100644 --- a/aries_cloudagent/anoncreds/default/legacy_indy/registry.py +++ b/aries_cloudagent/anoncreds/default/legacy_indy/registry.py @@ -4,7 +4,6 @@ import logging import re from typing import Optional, Pattern, Tuple -from urllib.parse import urlparse from ....config.injection_context import InjectionContext from ....core.profile import Profile @@ -21,7 +20,6 @@ ) from ....multitenant.base import BaseMultitenantManager from ....revocation.anoncreds import AnonCredsRevocation -from ....revocation.error import RevocationError from ....revocation.models.issuer_cred_rev_record import IssuerCredRevRecord from ....revocation.recover import generate_ledger_rrrecovery_txn from ....storage.error import StorageNotFoundError @@ -374,11 +372,6 @@ async def get_revocation_registry_definition( async def get_revocation_registry_definitions(self, profile: Profile, filter: str): """Get credential definition ids filtered by filter""" - def _check_url(self, url) -> None: - parsed = urlparse(url) - if not (parsed.scheme and parsed.netloc and parsed.path): - raise RevocationError("URI {} is not a valid URL".format(url)) - async def register_revocation_registry_definition( self, profile: Profile, @@ -387,14 +380,7 @@ async def register_revocation_registry_definition( ) -> RevRegDefResult: """Register a revocation registry definition on the registry.""" - tails_base_url = profile.settings.get("tails_server_base_url") - if not tails_base_url: - raise AnonCredsRegistrationError("tails_server_base_url not configured") - rev_reg_def_id = self.make_rev_reg_def_id(revocation_registry_definition) - revocation_registry_definition.value.tails_location = ( - tails_base_url.rstrip("/") + f"/{rev_reg_def_id}" - ) try: self._check_url(revocation_registry_definition.value.tails_location) @@ -474,7 +460,7 @@ async def _revoc_reg_entry_with_fix( profile, rev_list, True, - ledger.pool.genesis_txns, + ledger.genesis_txns, ) rev_entry_res = {"result": res} LOGGER.warn("Ledger update/fix applied") @@ -504,10 +490,7 @@ async def register_revocation_list( options: Optional[dict] = None, ) -> RevListResult: """Register a revocation list on the registry.""" - rev_reg_entry = { - "ver": "1.0", - "value": {"accum": rev_list.current_accumulator} - } + rev_reg_entry = {"ver": "1.0", "value": {"accum": rev_list.current_accumulator}} rev_entry_res = await self._revoc_reg_entry_with_fix( profile, rev_list, rev_reg_def.type, rev_reg_entry @@ -578,7 +561,9 @@ async def fix_ledger_entry( # get rev reg delta (revocations published to ledger) ledger = profile.inject(BaseLedger) async with ledger: - (rev_reg_delta, _) = await ledger.get_revoc_reg_delta(rev_list.rev_reg_def_id) + (rev_reg_delta, _) = await ledger.get_revoc_reg_delta( + rev_list.rev_reg_def_id + ) # get rev reg records from wallet (revocations and list) recs = [] diff --git a/aries_cloudagent/anoncreds/issuer.py b/aries_cloudagent/anoncreds/issuer.py index d7c2f8c668..232bca8e94 100644 --- a/aries_cloudagent/anoncreds/issuer.py +++ b/aries_cloudagent/anoncreds/issuer.py @@ -2,8 +2,11 @@ import asyncio import logging +import os +from pathlib import Path from time import time from typing import NamedTuple, Optional, Sequence, Tuple +from urllib.parse import urlparse from anoncreds import ( AnoncredsError, @@ -20,13 +23,14 @@ from ..askar.profile import AskarProfile from ..core.error import BaseError -from .base import AnonCredsSchemaAlreadyExists +from ..tails.base import BaseTailsServer +from .base import AnonCredsRegistrationError, AnonCredsSchemaAlreadyExists from .models.anoncreds_cred_def import CredDef, CredDefResult, CredDefState from .models.anoncreds_revocation import ( + RevList, RevRegDef, RevRegDefResult, RevRegDefState, - RevList, ) from .models.anoncreds_schema import AnonCredsSchema, SchemaResult, SchemaState from .registry import AnonCredsRegistry @@ -36,10 +40,10 @@ DEFAULT_CRED_DEF_TAG = "default" DEFAULT_SIGNATURE_TYPE = "CL" +CATEGORY_SCHEMA = "schema" CATEGORY_CRED_DEF = "credential_def" CATEGORY_CRED_DEF_PRIVATE = "credential_def_private" CATEGORY_CRED_DEF_KEY_PROOF = "credential_def_key_proof" -CATEGORY_SCHEMA = "schema" CATEGORY_REV_LIST = "revocation_list" CATEGORY_REV_REG_INFO = "revocation_reg_info" CATEGORY_REV_REG_DEF = "revocation_reg_def" @@ -104,7 +108,7 @@ async def _update_entry_state(self, category: str, name: str, state: str): async def _store_schema( self, - schema_id: str, + record_id: str, schema: AnonCredsSchema, state: str, ): @@ -113,7 +117,7 @@ async def _store_schema( async with self._profile.session() as session: await session.handle.insert( CATEGORY_SCHEMA, - schema_id, + record_id, schema.to_json(), { "name": schema.name, @@ -175,7 +179,7 @@ async def create_and_register_schema( ) await self._store_schema( - schema_result.schema_state.schema_id, + schema_result.schema_state.schema_id or schema_result.job_id, schema_result.schema_state.schema, state=schema_result.schema_state.state, ) @@ -273,14 +277,14 @@ async def create_and_register_credential_definition( Create a new credential definition and store it in the wallet. Args: - origin_did: the DID issuing the credential definition - schema_json: the schema used as a basis - signature_type: the credential definition signature type (default 'CL') - tag: the credential definition tag - support_revocation: whether to enable revocation for this credential def + issuer_id: the ID of the issuer creating the credential definition + schema_id: the schema ID for the credential definition + tag: the tag to use for the credential definition + signature_type: the signature type to use for the credential definition + options: any additional options to use when creating the credential definition Returns: - A tuple of the credential definition ID and JSON + CredDefResult: the result of the credential definition creation """ anoncreds_registry = self._profile.inject(AnonCredsRegistry) @@ -453,19 +457,18 @@ async def create_and_register_revocation_registry_definition( options: Optional[dict] = None, ) -> RevRegDefResult: """ - Create a new revocation registry and store it in the wallet. + Create a new revocation registry and register on network. Args: - origin_did: the DID issuing the revocation registry - cred_def_id: the identifier of the related credential definition - revoc_def_type: the revocation registry type (default CL_ACCUM) - tag: the unique revocation registry tag - max_cred_num: the number of credentials supported in the registry - tails_base_path: where to store the tails file - issuance_type: optionally override the issuance type + issuer_id (str): issuer identifier + cred_def_id (str): credential definition identifier + registry_type (str): revocation registry type + tag (str): revocation registry tag + max_cred_num (int): maximum number of credentials supported + options (dict): revocation registry options Returns: - A tuple of the revocation registry ID, JSON, and entry JSON + RevRegDefResult: revocation registry definition result """ try: @@ -499,15 +502,17 @@ async def create_and_register_revocation_registry_definition( tails_dir_path=tails_dir, ), ) - # TODO Move tails file to more human friendly folder structure? except AnoncredsError as err: raise AnonCredsIssuerError("Error creating revocation registry") from err rev_reg_def_json = rev_reg_def.to_json() + rev_reg_def = RevRegDef.from_native(rev_reg_def) + public_tails_uri = self.get_public_tails_uri(rev_reg_def) + rev_reg_def.value.tails_location = public_tails_uri anoncreds_registry = self.profile.inject(AnonCredsRegistry) result = await anoncreds_registry.register_revocation_registry_definition( - self.profile, RevRegDef.from_native(rev_reg_def), options + self.profile, rev_reg_def, options ) rev_reg_def_id = result.rev_reg_def_id @@ -536,6 +541,56 @@ async def create_and_register_revocation_registry_definition( return result + def _check_url(self, url) -> None: + parsed = urlparse(url) + if not (parsed.scheme and parsed.netloc and parsed.path): + raise AnonCredsRegistrationError("URI {} is not a valid URL".format(url)) + + def get_public_tails_uri(self, rev_reg_def: RevRegDef): + """Construct tails uri from rev_reg_def.""" + tails_base_url = self._profile.settings.get("tails_server_base_url") + if not tails_base_url: + raise AnonCredsRegistrationError("tails_server_base_url not configured") + + public_tails_uri = ( + tails_base_url.rstrip("/") + f"/{rev_reg_def.value.tails_hash}" + ) + + self._check_url(public_tails_uri) + return public_tails_uri + + def get_local_tails_path(self, rev_reg_def: RevRegDef) -> str: + """Get the local path to the tails file.""" + tails_dir = indy_client_dir("tails", create=False) + return os.path.join(tails_dir, rev_reg_def.value.tails_hash) + + async def upload_tails_file(self, rev_reg_def: RevRegDef): + """Upload the local tails file to the tails server.""" + tails_server = self._profile.inject_or(BaseTailsServer) + if not tails_server: + raise AnonCredsIssuerError("Tails server not configured") + if not Path(self.get_local_tails_path(rev_reg_def)).is_file(): + raise AnonCredsIssuerError("Local tails file not found") + + (upload_success, result) = await tails_server.upload_tails_file( + self._profile.context, + rev_reg_def.value.tails_hash, + self.get_local_tails_path(rev_reg_def), + interval=0.8, + backoff=-0.5, + max_attempts=5, # heuristic: respect HTTP timeout + ) + if not upload_success: + raise AnonCredsIssuerError( + f"Tails file for rev reg for {rev_reg_def.cred_def_id} " + "failed to upload: {result}" + ) + if rev_reg_def.value.tails_location != result: + raise AnonCredsIssuerError( + f"Tails file for rev reg for {rev_reg_def.cred_def_id} " + "uploaded to wrong location: {result}" + ) + async def update_revocation_registry_definition_state( self, rev_reg_def_id: str, state: str ): diff --git a/aries_cloudagent/config/default_context.py b/aries_cloudagent/config/default_context.py index 8cb951c4d4..e9dfd85599 100644 --- a/aries_cloudagent/config/default_context.py +++ b/aries_cloudagent/config/default_context.py @@ -84,7 +84,7 @@ async def bind_providers(self, context: InjectionContext): context.injector.bind_provider( BaseTailsServer, ClassProvider( - "aries_cloudagent.tails.indy_tails_server.IndyTailsServer", + "aries_cloudagent.tails.anoncreds_tails_server.AnonCredsTailsServer", ), ) diff --git a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py index cdfe47a191..0951355509 100644 --- a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py @@ -198,7 +198,7 @@ async def create_and_register_def(self, profile: Profile): self.state = IssuerRevRegRecord.STATE_POSTED self.tails_hash = result.rev_reg_def.value.tails_hash self.tails_public_uri = result.rev_reg_def.value.tails_location - self.tails_local_path = result.rev_reg_def.value.tails_location + self.tails_local_path = issuer.get_local_tails_path(result.rev_reg_def) async with profile.session() as session: await self.save(session, reason="Generated registry") diff --git a/aries_cloudagent/tails/anoncreds_tails_server.py b/aries_cloudagent/tails/anoncreds_tails_server.py new file mode 100644 index 0000000000..bb175dbfc9 --- /dev/null +++ b/aries_cloudagent/tails/anoncreds_tails_server.py @@ -0,0 +1,64 @@ +"""AnonCreds tails server interface class.""" + +import logging + +from typing import Tuple + +from ..config.injection_context import InjectionContext +from ..utils.http import put_file, PutError + +from .base import BaseTailsServer +from .error import TailsServerNotConfiguredError + + +LOGGER = logging.getLogger(__name__) + + +class AnonCredsTailsServer(BaseTailsServer): + """AnonCreds tails server interface.""" + + async def upload_tails_file( + self, + context: InjectionContext, + filename: str, + tails_file_path: str, + interval: float = 1.0, + backoff: float = 0.25, + max_attempts: int = 5, + ) -> Tuple[bool, str]: + """Upload tails file to tails server. + + Args: + context: context with configuration settings + filename: file name given to tails server + tails_file_path: path to the tails file to upload + interval: initial interval between attempts + backoff: exponential backoff in retry interval + max_attempts: maximum number of attempts to make + + Returns: + Tuple[bool, str]: tuple with success status and url of uploaded + file or error message if failed + """ + tails_server_upload_url = context.settings.get("tails_server_upload_url") + + if not tails_server_upload_url: + raise TailsServerNotConfiguredError( + "tails_server_upload_url setting is not set" + ) + + upload_url = tails_server_upload_url.rstrip("/") + f"/{filename}" + + try: + await put_file( + upload_url, + {"tails": tails_file_path}, + {}, + interval=interval, + backoff=backoff, + max_attempts=max_attempts, + ) + except PutError as x_put: + return (False, x_put.message) + + return True, upload_url diff --git a/aries_cloudagent/tails/base.py b/aries_cloudagent/tails/base.py index 7d4b82186b..f3c56f4527 100644 --- a/aries_cloudagent/tails/base.py +++ b/aries_cloudagent/tails/base.py @@ -13,7 +13,7 @@ class BaseTailsServer(ABC, metaclass=ABCMeta): async def upload_tails_file( self, context: InjectionContext, - rev_reg_id: str, + filename: str, tails_file_path: str, interval: float = 1.0, backoff: float = 0.25, @@ -22,9 +22,14 @@ async def upload_tails_file( """Upload tails file to tails server. Args: - rev_reg_id: The revocation registry identifier + context: context with configuration settings + filename: file name given to tails server tails_file: The path to the tails file to upload interval: initial interval between attempts backoff: exponential backoff in retry interval max_attempts: maximum number of attempts to make + + Returns: + Tuple[bool, str]: tuple with success status and url of uploaded + file or error message if failed """ diff --git a/aries_cloudagent/tails/indy_tails_server.py b/aries_cloudagent/tails/indy_tails_server.py index 0c5ebb6ab4..0b6d85e734 100644 --- a/aries_cloudagent/tails/indy_tails_server.py +++ b/aries_cloudagent/tails/indy_tails_server.py @@ -21,7 +21,7 @@ class IndyTailsServer(BaseTailsServer): async def upload_tails_file( self, context: InjectionContext, - rev_reg_id: str, + filename: str, tails_file_path: str, interval: float = 1.0, backoff: float = 0.25, @@ -31,11 +31,15 @@ async def upload_tails_file( Args: context: context with configuration settings - rev_reg_id: revocation registry identifier + filename: file name given to tails server tails_file_path: path to the tails file to upload interval: initial interval between attempts backoff: exponential backoff in retry interval max_attempts: maximum number of attempts to make + + Returns: + Tuple[bool, str]: tuple with success status and url of uploaded + file or error message if failed """ tails_server_upload_url = context.settings.get("tails_server_upload_url") genesis_transactions = context.settings.get("ledger.genesis_transactions") @@ -59,7 +63,7 @@ async def upload_tails_file( "tails_server_upload_url setting is not set" ) - upload_url = tails_server_upload_url.rstrip("/") + f"/{rev_reg_id}" + upload_url = tails_server_upload_url.rstrip("/") + f"/{filename}" try: await put_file(