Skip to content

Commit

Permalink
feat: add generic tails file upload
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
  • Loading branch information
dbluhm committed May 4, 2023
1 parent a2bb0c2 commit 2736000
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 50 deletions.
25 changes: 5 additions & 20 deletions aries_cloudagent/anoncreds/default/legacy_indy/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = []
Expand Down
101 changes: 78 additions & 23 deletions aries_cloudagent/anoncreds/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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,
):
Expand All @@ -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,
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
):
Expand Down
2 changes: 1 addition & 1 deletion aries_cloudagent/config/default_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
64 changes: 64 additions & 0 deletions aries_cloudagent/tails/anoncreds_tails_server.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 7 additions & 2 deletions aries_cloudagent/tails/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
"""
Loading

0 comments on commit 2736000

Please sign in to comment.