Exemple #1
0
async def list_tails(request: Request, ident: str) -> HTTPResponse:
    """
    List 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: HTTP response with JSON array of rev reg ids corresponding to available tails files
    """

    rv = []
    dir_tails = join(dirname(dirname(realpath(__file__))), 'tails')

    if ident == 'all':  # list everything: 'all' is not valid base58 so it can't be any case below
        rv = [basename(link) for link in Tails.links(dir_tails)]
    elif ok_rev_reg_id(ident):  # it's a rev reg id
        if Tails.linked(dir_tails, ident):
            rv = [ident]
    elif ok_cred_def_id(ident):  # it's a cred def id (starts with issuer DID)
        rv = [basename(link) for link in Tails.links(dir_tails, ident.split(':')[0])
            if rev_reg_id2cred_def_id(basename(link)) == ident]
    elif ok_did(ident):  # it's an issuer DID
        rv = [basename(link) for link in Tails.links(dir_tails, ident)]
    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('Fulfilling GET request listing tails files on filter %s', ident)
    return response.json(rv)
Exemple #2
0
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))
Exemple #3
0
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))
Exemple #4
0
async def list_tails(request, ident):
    """
    List 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: JSON array of rev reg ids corresponding to tails files available
    """

    rv = []
    dir_tails = pjoin(dirname(dirname(abspath(__file__))), 'tails')

    if ident == 'all':  # list everything: 'all' is not valid base58 so it can't be any case below
        rv = [basename(link) for link in Tails.links(dir_tails)]
    elif ok_rev_reg_id(ident) and Tails.linked(dir_tails, ident):  # it's a rev reg id
        rv = [ident]
    elif ok_cred_def_id(ident):  # it's a cred def id (starts with issuer DID)
        rv = [basename(link) for link in Tails.links(dir_tails, ident.split(':')[0])
            if rev_reg_id2cred_def_id(basename(link)) == ident]
    elif ok_did(ident):  # it's an issuer DID
        rv = [basename(link) for link in Tails.links(dir_tails, ident)]
    else:
        LOGGER.error("Token %s must be 'all', rev reg id, cred def id, or issuer DID", ident)
        raise InvalidUsage("Token {} must be 'all', rev reg id, cred def id, or issuer DID".format(ident))

    LOGGER.info('Fulfilling GET request listing tails files on filter %s', ident)
    return response.json(rv)
Exemple #5
0
    async def _set_rev_reg(self, rr_id: str, rr_size: int) -> None:
        """
        Move precomputed revocation registry data from hopper into place within tails directory.

        :param rr_id: revocation registry identifier
        :param rr_size: revocation registry size, in case creation required
        """

        LOGGER.debug('Issuer._set_rev_reg >>> rr_id: %s, rr_size: %s', rr_id, rr_size)
        assert self._rrbx

        dir_hopper_rr_id = join(self._dir_tails_hopper, rr_id)

        while Tails.linked(dir_hopper_rr_id, rr_id) is None:
            await asyncio.sleep(1)
        await self._send_rev_reg_def(rr_id)

        cd_id = rev_reg_id2cred_def_id(rr_id)
        (next_tag, rr_size_suggested) = Tails.next_tag(self._dir_tails, cd_id)
        rr_id = rev_reg_id(cd_id, next_tag)
        try:
            makedirs(join(self._dir_tails_sentinel, rr_id), exist_ok=False)
        except FileExistsError:
            LOGGER.warning('Rev reg %s construction already in progress', rr_id)
        else:
            open(join(self._dir_tails_sentinel, rr_id, '.{}'.format(rr_size or rr_size_suggested)), 'w').close()

        LOGGER.debug('Issuer._set_rev_reg <<<')
Exemple #6
0
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('')
Exemple #7
0
    def path_tails(self, rr_id: str) -> str:
        """
        Return path to tails file for input revocation registry identifier.

        :param rr_id: revocation registry identifier of interest
        :return: path to tails file for input revocation registry identifier
        """

        LOGGER.debug('Issuer.path_tails >>>')

        if not ok_rev_reg_id(rr_id):
            LOGGER.debug('Issuer.path_tails <!< Bad rev reg id %s', rr_id)
            raise BadIdentifier('Bad rev reg id {}'.format(rr_id))

        rv = Tails.linked(self._dir_tails, rr_id)
        LOGGER.debug('Issuer.path_tails <<< %s', rv)
        return rv
Exemple #8
0
async def sync_issuer(
        dir_tails: str,
        host: str,
        port: int,
        local_only: set,
        noman: NominalAnchor) -> None:
    """
    Synchronize for issuer: upload any tails files appearing locally but not remotely.

    :param dir_tails: local tails directory
    :param host: tails server host
    :param port: tails server port
    :param local_only: paths to local tails symbolic links (rev reg ids) without corresponding remote tails files
    :param noman: open issuer anchor
    """

    logging.debug('Sync-issuer: local-only=%s', ppjson(local_only))
    if not local_only:
        return

    for rr_id in local_only:
        if not ok_rev_reg_id(rr_id, noman.did):  # restrict POSTs to issuer's own tails files
            logging.debug(
                'Sync-issuer: local-only %s is not a rev reg id for issuer %s (%s)',
                rr_id,
                noman.did,
                noman.wallet.name)
            continue

        epoch = int(time())
        url = 'http://{}:{}/tails/{}/{}'.format(host, port, quote(rr_id), epoch)
        path_tails = Tails.linked(dir_tails, rr_id)
        with open(path_tails, 'rb') as tails_fh:
            tails = tails_fh.read()
            sig = await noman.sign('{}||{}'.format(epoch, tails))
        try:
            resp = requests.post(
                url,
                files={
                    'tails-file': (basename(path_tails), tails),
                    'signature': ('signature', sig)
                })
            logging.info('Upload: url %s status %s', url, resp.status_code)
        except RequestsConnectionError:
            logging.error('POST connection refused: %s', url)
Exemple #9
0
def sync_issuer(dir_tails, host, port, local_only):
    """
    Synchronize for issuer: upload any tails files appearing locally but not remotely.

    :param dir_tails: local root tails directory
    :param host: tails server host
    :param port: tails server port
    :param local_only: paths to local tails symbolic links (rev reg ids) without corresponding remote tails files
    """

    for rr_id in local_only:
        url = 'http://{}:{}/tails/{}'.format(host, port, rr_id)
        with open(Tails.linked(dir_tails, rr_id), 'rb') as fh_tails:
            try:
                resp = requests.post(url, files={'file': fh_tails})
                logging.info('Uploaded: url %s status %s', url,
                             resp.status_code)
            except ConnectionError:
                logging.error('POST Connection refused: %s', url)
Exemple #10
0
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('')
Exemple #11
0
    async def _set_rev_reg(self, rr_id: str, rr_size: int) -> None:
        """
        Move precomputed revocation registry data from hopper into place within tails directory.

        :param rr_id: revocation registry identifier
        :param rr_size: revocation registry size, in case creation required
        """

        LOGGER.debug('Issuer._set_rev_reg >>> rr_id: %s, rr_size: %s', rr_id,
                     rr_size)
        assert self.rrbx

        dir_hopper_rr_id = join(self.rrb.dir_tails_hopper, rr_id)

        while Tails.linked(dir_hopper_rr_id, rr_id) is None:
            await asyncio.sleep(1)
        await self._send_rev_reg_def(rr_id)

        cd_id = rev_reg_id2cred_def_id(rr_id)
        (next_tag, rr_size_suggested) = Tails.next_tag(self.dir_tails, cd_id)
        rr_id = rev_reg_id(cd_id, next_tag)
        self.rrb.mark_in_progress(rr_id, rr_size or rr_size_suggested)

        LOGGER.debug('Issuer._set_rev_reg <<<')
Exemple #12
0
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('')
Exemple #13
0
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('')
Exemple #14
0
    async def _send_rev_reg_def(self, rr_id: str) -> None:
        """
        Move tails file from hopper; deserialize revocation registry definition and initial entry;
        send to ledger and cache revocation registry definition.

        Operation serializes to subdirectory within tails hopper directory; symbolic
        link presence signals completion.

        Raise AbsentRevReg if revocation registry is not ready in hopper, or AbsentTails if
        tails file is not yet linked by its revocation registry identifier.

        :param rr_id: revocation registry identifier
        """

        LOGGER.debug('Issuer._send_rev_reg_def >>> rr_id: %s', rr_id)

        dir_tails_rr_id = self.rrb.dir_tails_top(rr_id)
        dir_target = self.rrb.dir_tails_target(rr_id)

        if not Tails.linked(dir_tails_rr_id, rr_id):
            LOGGER.debug(
                'Issuer._send_rev_reg_def <!< Tails file for rev reg %s not ready in dir %s',
                rr_id, dir_target)
            raise AbsentRevReg(
                'Tails file for rev reg {} not ready in dir {}'.format(
                    rr_id, dir_target))

        file_rr_def = join(dir_target, 'rr_def.json')
        if not isfile(file_rr_def):
            LOGGER.debug(
                'Issuer._send_rev_reg_def <!< Rev reg def file %s not present',
                file_rr_def)
            raise AbsentRevReg(
                'Rev reg def file {} not present'.format(file_rr_def))
        with open(file_rr_def, 'r') as fh_rr_def:
            rr_def_json = fh_rr_def.read()

        file_rr_ent = join(dir_target, 'rr_ent.json')
        if not isfile(file_rr_ent):
            LOGGER.debug(
                'Issuer._send_rev_reg_def <!< Rev reg entry file %s not present',
                file_rr_ent)
            raise AbsentRevReg(
                'Rev reg entry file {} not present'.format(file_rr_ent))
        with open(file_rr_ent, 'r') as fh_rr_ent:
            rr_ent_json = fh_rr_ent.read()

        file_tails = Tails.linked(dir_tails_rr_id, rr_id)
        if not file_tails:
            LOGGER.debug(
                'Issuer._send_rev_reg_def <!< Tails link %s not present in dir %s',
                rr_id, dir_target)
            raise AbsentTails('Tails link {} not present in dir {}'.format(
                rr_id, dir_target))

        if self.rrbx:
            dir_cd_id = join(self.dir_tails, rev_reg_id2cred_def_id(rr_id))
            makedirs(dir_cd_id, exist_ok=True)
            rename(file_tails, join(dir_cd_id, basename(file_tails)))

        with REVO_CACHE.lock:
            rr_def_req_json = await ledger.build_revoc_reg_def_request(
                self.did, rr_def_json)
            await self._sign_submit(rr_def_req_json)
            await self.get_rev_reg_def(rr_id)  # add to cache en passant

        rr_ent_req_json = await ledger.build_revoc_reg_entry_request(
            self.did, rr_id, 'CL_ACCUM', rr_ent_json)
        await self._sign_submit(rr_ent_req_json)

        if self.rrbx:
            Tails.associate(self.dir_tails, rr_id, basename(file_tails))
            rmtree(dir_tails_rr_id)
        else:
            remove(file_rr_def)
            remove(file_rr_ent)

        LOGGER.debug('Issuer._send_rev_reg_def <<<')