async def get_tails(request: Request, rr_id: str) -> HTTPResponse: """ Get tails file pertaining to input revocation registry identifier. :param request: Sanic request structure :param rr_id: rev reg id for revocation registry to which tails file pertains :return: HTTP response with tails file, having tails hash as name """ if not ok_rev_reg_id(rr_id): LOGGER.error('GET cited bad rev reg id %s', rr_id) return response.text('GET cited bad rev reg id {}'.format(rr_id), status=400) dir_tails = join(dirname(dirname(realpath(__file__))), 'tails') dir_cd_id = Tails.dir(dir_tails, rr_id) if not isdir(dir_cd_id): LOGGER.error('GET cited rev reg id %s for which tails file dir %s not present', rr_id, dir_cd_id) return response.text( 'GET cited rev reg id {} for which tails file dir {} not present'.format(rr_id, dir_cd_id), status=404) path_tails = Tails.linked(dir_tails, rr_id) if not path_tails: LOGGER.error('GET cited rev reg id %s for which tails file not present', rr_id) return response.text('GET cited rev reg id {} for which tails file not present'.format(rr_id), status=404) LOGGER.info('Fulfilling download GET request for tails file %s associated with rev reg id %s', path_tails, rr_id) return await response.file(path_tails, filename=basename(path_tails))
async def get_tails(request, rr_id): """ Get tails file pertaining to input revocation registry identifier. :param request: Sanic request structure :param rr_id: rev reg id for revocation registry to which tails file pertains :return: tails file, with tails hash as name """ if not ok_rev_reg_id(rr_id): LOGGER.error('GET cited bad rev reg id %s', rr_id) raise InvalidUsage('GET cited bad rev reg id {}'.format(rr_id)) dir_tails = pjoin(dirname(dirname(abspath(__file__))), 'tails') dir_cd_id = Tails.dir(dir_tails, rr_id) if not isdir(dir_cd_id): LOGGER.error('GET cited rev reg id %s for which tails file dir %s not present', rr_id, dir_cd_id) raise NotFound('GET cited rev reg id {} for which tails file dir {} not present'.format(rr_id, dir_cd_id)) path_tails = Tails.linked(dir_tails, rr_id) if not path_tails: LOGGER.error('GET cited rev reg id %s for which tails file not present', rr_id) raise NotFound('GET cited rev reg id {} for which tails file not present'.format(rr_id)) LOGGER.info('Fulfilling download GET request for tails file %s associated with rev reg id %s', path_tails, rr_id) return await response.file(path_tails, filename=basename(path_tails))
async def _create_rev_reg(self, rr_id: str, rr_size: int = None) -> None: """ Create revocation registry and new tails file (and association to corresponding revocation registry definition via symbolic link) for input revocation registry identifier. :param rr_id: revocation registry identifier :param rr_size: revocation registry size (defaults to 256) """ LOGGER.debug('Issuer._create_rev_reg >>> rr_id: %s, rr_size: %s', rr_id, rr_size) if not ok_rev_reg_id(rr_id): LOGGER.debug('Issuer._create_rev_reg <!< Bad rev reg id %s', rr_id) raise BadIdentifier('Bad rev reg id {}'.format(rr_id)) rr_size = rr_size or 256 (cd_id, tag) = rev_reg_id2cred_def_id_tag(rr_id) LOGGER.info( 'Creating revocation registry (capacity %s) for rev reg id %s', rr_size, rr_id) tails_writer_handle = await blob_storage.open_writer( 'default', json.dumps({ 'base_dir': Tails.dir(self._dir_tails, rr_id), 'uri_pattern': '' })) apriori = Tails.unlinked(self._dir_tails) (rr_id, rrd_json, rre_json) = await anoncreds.issuer_create_and_store_revoc_reg( self.wallet.handle, self.did, 'CL_ACCUM', tag, cd_id, json.dumps({ 'max_cred_num': rr_size, 'issuance_type': 'ISSUANCE_ON_DEMAND' }), tails_writer_handle) delta = Tails.unlinked(self._dir_tails) - apriori if len(delta) != 1: LOGGER.debug( 'Issuer._create_rev_reg: <!< Could not create tails file for rev reg id: %s', rr_id) raise CorruptTails( 'Could not create tails file for rev reg id {}'.format(rr_id)) tails_hash = basename(delta.pop()) Tails.associate(self._dir_tails, rr_id, tails_hash) with REVO_CACHE.lock: rrd_req_json = await ledger.build_revoc_reg_def_request( self.did, rrd_json) await self._sign_submit(rrd_req_json) await self._get_rev_reg_def(rr_id) # add to cache en passant rre_req_json = await ledger.build_revoc_reg_entry_request( self.did, rr_id, 'CL_ACCUM', rre_json) await self._sign_submit(rre_req_json) LOGGER.debug('Issuer._create_rev_reg <<<')
async def delete_tails(request, ident): """ Delete tails files by corresponding rev reg ids: all, by rev reg id, by cred def id, or by issuer DID. :param request: Sanic request structure :param ident: 'all' for no filter; rev reg id, cred def id, or issuer DID to filter by any such identifier :return: empty text string """ dir_tails = pjoin(dirname(dirname(abspath(__file__))), 'tails') if ident == 'all': # delete everything: 'all' is not valid base58 so it can't be any case below rmtree(dir_tails) makedirs(dir_tails, exist_ok=True) elif ok_rev_reg_id(ident): # it's a rev reg id path_tails = Tails.linked(dir_tails, ident) if path_tails and isfile(path_tails): unlink(path_tails) LOGGER.info('Deleted %s', path_tails) path_link = pjoin(Tails.dir(dir_tails, ident), ident) if path_link and islink(path_link): unlink(path_link) LOGGER.info('Deleted %s', path_link) elif ok_cred_def_id(ident): # it's a cred def id (starts with issuer DID) dir_cd_id = pjoin(dir_tails, ident) if isdir(dir_cd_id): rmtree(dir_cd_id) LOGGER.info('Deleted %s', dir_cd_id) elif exists(dir_cd_id): # non-dir is squatting on name reserved for dir: it's corrupt; remove it unlink(dir_cd_id) LOGGER.info('Deleted spurious non-directory %s', dir_cd_id) elif ok_did(ident): # it's an issuer DID dirs_cd_id = {dirname(link) for link in Tails.links(dir_tails, ident)} for dir_cd_id in dirs_cd_id: if ok_cred_def_id(basename(dir_cd_id)): if isdir(dir_cd_id): rmtree(dir_cd_id) LOGGER.info('Deleted %s', dir_cd_id) elif exists(dir_cd_id): # non-dir is squatting on name reserved for dir: it's corrupt; remove it unlink(dir_cd_id) LOGGER.info('Deleted spurious non-directory %s', dir_cd_id) else: LOGGER.error('Token %s must be rev reg id, cred def id, or issuer DID', ident) raise InvalidUsage('Token {} must be rev reg id, cred def id, or issuer DID'.format(ident)) LOGGER.info('Fulfilled DELETE request deleting tails files on filter %s', ident) return response.text('')
async def post_tails(request, rr_id): """ Post tails file to server. Multipart file name must be tails hash. :param request: Sanic request structure :param rr_id: rev reg id for revocation registry to which tails file pertains :return: empty text string """ if not ok_rev_reg_id(rr_id): LOGGER.error('POST cited bad rev reg id %s', rr_id) raise InvalidUsage('POST cited bad rev reg id {}'.format(rr_id)) # curl uses 'data', python requests uses 'file', there may be others req_key = set(k for k in request.files if request.files[k] and isinstance(request.files[k], list) and isinstance(request.files[k][0], SanicReqFile)).pop() tails_hash = request.files[req_key][0].name if not Tails.ok_hash(tails_hash): LOGGER.error('POST attached file named with bad tails file hash %s', tails_hash) raise InvalidUsage('POST attached file named with bad tails file hash {}'.format(tails_hash)) dir_tails = pjoin(dirname(dirname(abspath(__file__))), 'tails') dir_cd_id = Tails.dir(dir_tails, rr_id) makedirs(dir_cd_id, exist_ok=True) if Tails.linked(dir_tails, rr_id): LOGGER.error('POST attached tails file %s, already present', rr_id) raise Forbidden('POST attached tails file {}, already present'.format(rr_id)) path_tails_hash = pjoin(dir_cd_id, tails_hash) if exists(path_tails_hash): LOGGER.error('POST attached tails file %s, already present at %s', rr_id, path_tails_hash) raise Forbidden('POST attached tails file {}, already present at {}'.format(rr_id, path_tails_hash)) with open(path_tails_hash, 'wb') as fh_tails: fh_tails.write(request.files[req_key][0].body) Tails.associate(dir_tails, rr_id, tails_hash) LOGGER.info('Associated link %s to POST tails file attachment saved to %s', rr_id, path_tails_hash) return response.text('')
async def sync_prover(dir_tails: str, host: str, port: int, remote_only: set) -> None: """ Synchronize for prover: download any tails files appearing remotely but not locally. :param dir_tails: local tails directory :param host: tails server host :param port: tails server port :param remote_only: paths to remote rev reg ids without corresponding local tails files """ if not remote_only: return for rr_id in remote_only: dir_cd_id = Tails.dir(dir_tails, rr_id) makedirs(dir_cd_id, exist_ok=True) url = 'http://{}:{}/tails/{}'.format(host, port, rr_id) try: resp = requests.get(url, stream=True) if resp.status_code == requests.codes.ok: re_tails_hash = re.search('filename="(.+)"', resp.headers['content-disposition']) tails_hash = re_tails_hash.group( 1) if re_tails_hash.lastindex > 0 else None if tails_hash: logging.info('Downloaded: url %s tails-hash %s', url, tails_hash) with open(join(dir_cd_id, tails_hash), 'wb') as fh_tails: for chunk in resp.iter_content(chunk_size=1024): if chunk: fh_tails.write(chunk) Tails.associate(dir_tails, rr_id, tails_hash) else: logging.error( 'Download: url %s, responded with no tails-hash', url) else: logging.error('Download: url %s, responded with status %s', url, resp.status_code) except RequestsConnectionError: logging.error('GET connection refused: %s', url)
async def post_tails(request: Request, rr_id: str, epoch: int) -> HTTPResponse: """ Post tails file to server, auth-encrypted from issuer (by DID) to tails server anchor. Multipart file name must be tails hash. :param request: Sanic request structure :param rr_id: revocation registry identifier :param epoch: current EPOCH time, must be within configured proximity to current server time :return: empty text response """ if not ok_rev_reg_id(rr_id): LOGGER.error('POST cited bad rev reg id %s', rr_id) return response.text('POST cited bad rev reg id {}'.format(rr_id), status=400) did = rr_id.split(':')[0] if not await is_current(int(epoch)): LOGGER.error('POST epoch %s in too far from current server time', epoch) return response.text('POST epoch {} is too far from current server time'.format(epoch), status=400) tails_hash = request.files['tails-file'][0].name if not Tails.ok_hash(tails_hash): LOGGER.error('POST attached file named with bad tails file hash %s', tails_hash) return response.text('POST attached file named with bad tails file hash {}'.format(tails_hash), status=400) dir_tails = join(dirname(dirname(realpath(__file__))), 'tails') dir_cd_id = Tails.dir(dir_tails, rr_id) makedirs(dir_cd_id, exist_ok=True) if Tails.linked(dir_tails, rr_id): LOGGER.error('POST attached tails file %s, already present', rr_id) return response.text('POST attached tails file {}, already present'.format(rr_id), status=403) path_tails_hash = join(dir_cd_id, tails_hash) if exists(path_tails_hash): LOGGER.error('POST attached tails file %s, already present at %s', rr_id, path_tails_hash) return response.text( 'POST attached tails file {}, already present at {}'.format(rr_id, path_tails_hash), status=403) tsan = await MEM_CACHE.get('tsan') signature = request.files['signature'][0].body epoch_tails = '{}||{}'.format(epoch, request.files['tails-file'][0].body) if not tsan.verify(epoch_tails, signature, did): LOGGER.error('POST attached file %s failed to verify', tails_hash) return response.text('POST attached file {} failed to verify'.format(tails_hash), status=400) try: rev_reg_def = json.loads(await tsan.get_rev_reg_def(rr_id)) ledger_hash = rev_reg_def.get('value', {}).get('tailsHash', None) if ledger_hash != tails_hash: LOGGER.error('POST attached tails file hash %s differs from ledger value %s', tails_hash, ledger_hash) return response.text( 'POST attached tails file hash {} differs from ledger value {}'.format(tails_hash, ledger_hash), status=400) except AbsentRevReg: LOGGER.error('POST revocation registry not present on ledger for %s', rr_id) return response.text('POST revocation registry not present on ledger for {}'.format(rr_id), status=400) with open(path_tails_hash, 'wb') as fh_tails: fh_tails.write(request.files['tails-file'][0].body) Tails.associate(dir_tails, rr_id, tails_hash) LOGGER.info('Associated link %s to POST tails file attachment saved to %s', rr_id, path_tails_hash) return response.text('')
async def delete_tails(request: Request, ident: str, epoch: int) -> HTTPResponse: """ Delete tails files by corresponding rev reg ids: all, by rev reg id, by cred def id, or by issuer DID. :param request: Sanic request structure :param ident: 'all' for no filter; rev reg id, cred def id, or issuer DID to filter by any such identifier :param epoch: current EPOCH time, must be within 5 minutes of current server time :return: empty text response """ if not await is_current(int(epoch)): LOGGER.error('DELETE epoch %s in too far from current server time', epoch) return response.text('DELETE epoch {} is too far from current server time'.format(epoch), status=400) signature = request.body plain = '{}||{}'.format(epoch, ident) tsan = await MEM_CACHE.get('tsan') if not tsan.verify(plain, signature, tsan.did): LOGGER.error('DELETE signature failed to verify') return response.text('DELETE signature failed to verify', status=400) dir_tails = join(dirname(dirname(realpath(__file__))), 'tails') if ident == 'all': # delete everything -- note that 'all' is not valid base58 so no case below can apply if isdir(dir_tails): rmtree(dir_tails) makedirs(dir_tails, exist_ok=True) elif ok_rev_reg_id(ident): # it's a rev reg id path_tails = Tails.linked(dir_tails, ident) if path_tails and isfile(path_tails): unlink(path_tails) LOGGER.info('Deleted %s', path_tails) path_link = join(Tails.dir(dir_tails, ident), ident) if path_link and islink(path_link): unlink(path_link) LOGGER.info('Deleted %s', path_link) elif ok_cred_def_id(ident): # it's a cred def id (starts with issuer DID) dir_cd_id = join(dir_tails, ident) if isdir(dir_cd_id): rmtree(dir_cd_id) LOGGER.info('Deleted %s', dir_cd_id) elif exists(dir_cd_id): # non-dir is squatting on name reserved for dir: it's corrupt; remove it unlink(dir_cd_id) LOGGER.info('Deleted spurious non-directory %s', dir_cd_id) elif ok_did(ident): # it's an issuer DID dirs_cd_id = {dirname(link) for link in Tails.links(dir_tails, ident)} for dir_cd_id in dirs_cd_id: if ok_cred_def_id(basename(dir_cd_id)): if isdir(dir_cd_id): rmtree(dir_cd_id) LOGGER.info('Deleted %s', dir_cd_id) elif exists(dir_cd_id): # non-dir is squatting on name reserved for dir: it's corrupt; remove it unlink(dir_cd_id) LOGGER.info('Deleted spurious non-directory %s', dir_cd_id) else: LOGGER.error('Token %s is not a valid specifier for tails files', ident) return response.text('Token {} is not a valid specifier for tails files'.format(ident), status=400) LOGGER.info('Fulfilled DELETE request deleting tails files on filter %s', ident) return response.text('')