Esempio n. 1
0
    async def _sync_revoc(self, rr_id: str) -> None:
        """
        Pick up tails file reader handle for input revocation registry identifier.  If no symbolic
        link is present, get the revocation registry definition to retrieve its tails file hash,
        then find the tails file and link it.

        Raise AbsentTails for missing corresponding tails file.

        :param rr_id: revocation registry identifier
        """

        LOGGER.debug('HolderProver._sync_revoc >>> rr_id: %s', rr_id)

        (cd_id, tag) = rev_reg_id2cred_def_id__tag(rr_id)

        try:
            json.loads(await self.get_cred_def(cd_id))
        except AbsentCredDef:
            LOGGER.debug(
                'HolderProver._sync_revoc: <!< corrupt tails tree %s may be for another ledger',
                self._dir_tails)
            raise AbsentCredDef(
                'Corrupt tails tree {} may be for another ledger'.format(
                    self._dir_tails))
        except ClosedPool:
            pass  # carry on, may be OK from cache only

        with REVO_CACHE.lock:
            revo_cache_entry = REVO_CACHE.get(rr_id, None)
            tails = revo_cache_entry.tails if revo_cache_entry else None
            if tails is None:  #  it's not yet set in cache
                try:
                    tails = await Tails(self._dir_tails, cd_id, tag).open()
                except AbsentTails:  # get hash from ledger and check for tails file
                    rrdef = json.loads(await self._get_rev_reg_def(rr_id))
                    tails_hash = rrdef['value']['tailsHash']
                    path_tails = join(Tails.dir(self._dir_tails, rr_id),
                                      tails_hash)
                    if not isfile(path_tails):
                        LOGGER.debug(
                            'HolderProver._sync_revoc: <!< No tails file present at %s',
                            path_tails)
                        raise AbsentTails(
                            'No tails file present at {}'.format(path_tails))
                    Tails.associate(self._dir_tails, rr_id, tails_hash)
                    tails = await Tails(
                        self._dir_tails, cd_id,
                        tag).open()  # OK now since tails file present

                if revo_cache_entry is None:
                    REVO_CACHE[rr_id] = RevoCacheEntry(None, tails)
                else:
                    REVO_CACHE[rr_id].tails = tails

        LOGGER.debug('HolderProver._sync_revoc <<<')
Esempio n. 2
0
    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)

        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 <<<')
Esempio n. 3
0
    async def close(self) -> None:
        """
        Explicit exit. If so configured, populate cache to prove all creds in
        wallet offline if need be, archive cache, and purge prior cache archives.

        :return: current object
        """

        LOGGER.debug('HolderProver.close >>>')

        if self.cfg.get('archive-cache-on-close', False):
            await self.load_cache(True)
            Caches.purge_archives(self.dir_cache, True)

        await super().close()
        for path_rr_id in Tails.links(self._dir_tails):
            rr_id = basename(path_rr_id)
            try:
                await self._sync_revoc(rr_id)
            except ClosedPool:
                LOGGER.warning(
                    'HolderProver sync-revoc on close required ledger for %s but pool was closed',
                    rr_id)

        LOGGER.debug('HolderProver.close <<<')
Esempio n. 4
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
        """

        return Tails.linked(self._dir_tails, rr_id)
Esempio n. 5
0
    def dir_tails(self, rr_id: str) -> str:
        """
        Return path to the correct directory for the tails file on input revocation registry identifier.

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

        return Tails.dir(self._dir_tails, rr_id)
Esempio n. 6
0
    def rev_regs(self) -> list:
        """
        Return list of revocation registry identifiers for which HolderProver has tails files.

        :return: list of revocation registry identifiers for which HolderProver has tails files
        """

        LOGGER.debug('HolderProver.rev_regs >>>')

        rv = [basename(f) for f in Tails.links(self._dir_tails)]
        LOGGER.debug('HolderProver.rev_regs <<< %s', rv)
        return rv
Esempio n. 7
0
    async def open(self) -> 'Issuer':
        """
        Explicit entry. Perform ancestor opening operations,
        then synchronize revocation registry to tails tree content.

        :return: current object
        """

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

        await super().open()
        for path_rr_id in Tails.links(self._dir_tails, self.did):
            await self._sync_revoc(basename(path_rr_id))

        LOGGER.debug('Issuer.open <<<')
        return self
Esempio n. 8
0
    async def open(self) -> 'HolderProver':
        """
        Explicit entry. Perform ancestor opening operations,
        then parse cache from archive if so configured, and
        synchronize revocation registry to tails tree content.

        :return: current object
        """

        LOGGER.debug('HolderProver.open >>>')

        await super().open()
        if self.cfg.get('parse-cache-on-open', False):
            Caches.parse(self.dir_cache)

        for path_rr_id in Tails.links(self._dir_tails):
            await self._sync_revoc(basename(path_rr_id))

        LOGGER.debug('HolderProver.open <<<')
        return self
Esempio n. 9
0
    async def get_box_ids_json(self) -> str:
        """
        Return json object on lists of all unique box identifiers (schema identifiers,
        credential definition identifiers, and revocation registry identifiers) for
        all credential definitions and credentials issued; e.g.,

        ::

        {
            "schema_id": [
                "R17v42T4pk...:2:tombstone:1.2",
                ...
            ],
            "cred_def_id": [
                "R17v42T4pk...:3:CL:19:0",
                ...
            ]
            "rev_reg_id": [
                "R17v42T4pk...:4:R17v42T4pk...:3:CL:19:0:CL_ACCUM:0",
                "R17v42T4pk...:4:R17v42T4pk...:3:CL:19:0:CL_ACCUM:1",
                ...
            ]
        }

        An issuer must issue a credential definition to include its schema identifier
        in the returned values; the schema identifier in isolation belongs properly
        to an Origin, not necessarily to an Issuer.

        The operation may be useful for a Verifier agent going off-line to seed its
        cache before doing so.

        :return: tuple of sets for schema ids, cred def ids, rev reg ids
        """

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

        cd_ids = [
            d for d in listdir(self._dir_tails)
            if isdir(join(self._dir_tails, d))
            and d.startswith('{}:3:'.format(self.did))
        ]
        s_ids = []
        for cd_id in cd_ids:
            try:
                s_ids.append(
                    json.loads(await self.get_schema(cred_def_id2seq_no(cd_id)
                                                     ))['id'])
            except AbsentSchema:
                LOGGER.error(
                    'Issuer %s has issued cred def %s but no corresponding schema on ledger',
                    self.wallet.name, cd_id)
        rr_ids = [
            basename(link) for link in Tails.links(self._dir_tails, self.did)
        ]

        rv = json.dumps({
            'schema_id': s_ids,
            'cred_def_id': cd_ids,
            'rev_reg_id': rr_ids
        })
        LOGGER.debug('Issuer.get_box_ids_json <<< %s', rv)
        return rv
Esempio n. 10
0
    async def create_cred(self,
                          cred_offer_json,
                          cred_req_json: str,
                          cred_attrs: dict,
                          rr_size: int = None) -> (str, str, int):
        """
        Create credential as Issuer out of credential request and dict of key:value (raw, unencoded)
        entries for attributes.

        Return credential json, and if cred def supports revocation, credential revocation identifier
        and revocation registry delta ledger timestamp (epoch seconds).

        If the credential definition supports revocation, and the current revocation registry is full,
        the processing creates a new revocation registry en passant. Depending on the revocation
        registry size (by default starting at 256 and doubling iteratively through 4096), this
        operation may delay credential creation by several seconds.

        :param cred_offer_json: credential offer json as created by Issuer
        :param cred_req_json: credential request json as created by HolderProver
        :param cred_attrs: dict mapping each attribute to its raw value (the operation encodes it); e.g.,

        ::

            {
                'favourite_drink': 'martini',
                'height': 180,
                'last_visit_date': '2017-12-31',
                'weaknesses': None
            }

        :param rr_size: size of new revocation registry (default as per _create_rev_reg()) if necessary
        :return: newly issued credential json; credential revocation identifier (if cred def supports
            revocation, None otherwise), and ledger timestamp (if cred def supports revocation, None otherwise)
        """

        LOGGER.debug(
            'Issuer.create_cred >>> cred_offer_json: %s, cred_req_json: %s, cred_attrs: %s, rr_size: %s',
            cred_offer_json, cred_req_json, cred_attrs, rr_size)

        cd_id = json.loads(cred_offer_json)['cred_def_id']
        cred_def = json.loads(
            await self.get_cred_def(cd_id))  # ensure cred def is in cache

        if 'revocation' in cred_def['value']:
            with REVO_CACHE.lock:
                rr_id = Tails.current_rev_reg_id(self._dir_tails, cd_id)
                tails = REVO_CACHE[rr_id].tails
                assert tails  # at (re)start, at cred def, Issuer sync_revoc() sets this index in revocation cache

                try:
                    (cred_json, cred_revoc_id,
                     rr_delta_json) = await anoncreds.issuer_create_credential(
                         self.wallet.handle, cred_offer_json, cred_req_json,
                         json.dumps({
                             k: cred_attr_value(cred_attrs[k])
                             for k in cred_attrs
                         }), tails.rr_id, tails.reader_handle)
                    # do not create rr delta frame and append to cached delta frames list: timestamp could lag or skew
                    rre_req_json = await ledger.build_revoc_reg_entry_request(
                        self.did, tails.rr_id, 'CL_ACCUM', rr_delta_json)
                    await self._sign_submit(rre_req_json)
                    resp_json = await self._sign_submit(rre_req_json)
                    resp = json.loads(resp_json)
                    rv = (cred_json, cred_revoc_id,
                          resp['result']['txnMetadata']['txnTime'])

                except IndyError as x_indy:
                    if x_indy.error_code == ErrorCode.AnoncredsRevocationRegistryFullError:
                        (tag, rr_size_suggested) = Tails.next_tag(
                            self._dir_tails, cd_id)
                        rr_id = rev_reg_id(cd_id, tag)
                        await self._create_rev_reg(
                            rr_id, rr_size or rr_size_suggested)
                        REVO_CACHE[rr_id].tails = await Tails(
                            self._dir_tails, cd_id).open()
                        return await self.create_cred(cred_offer_json,
                                                      cred_req_json, cred_attrs
                                                      )  # should be ok now
                    else:
                        LOGGER.debug(
                            'Issuer.create_cred: <!<  cannot create cred, indy error code %s',
                            x_indy.error_code)
                        raise
        else:
            try:
                (cred_json, _, _) = await anoncreds.issuer_create_credential(
                    self.wallet.handle, cred_offer_json, cred_req_json,
                    json.dumps({
                        k: cred_attr_value(cred_attrs[k])
                        for k in cred_attrs
                    }), None, None)
                rv = (cred_json, _, _)
            except IndyError as x_indy:
                LOGGER.debug(
                    'Issuer.create_cred: <!<  cannot create cred, indy error code %s',
                    x_indy.error_code)
                raise

        LOGGER.debug('Issuer.create_cred <<< %s', rv)
        return rv
Esempio n. 11
0
    async def send_cred_def(self,
                            s_id: str,
                            revocation: bool = True,
                            rr_size: int = None) -> str:
        """
        Create a credential definition as Issuer, store it in its wallet, and send it to the ledger.

        Raise CorruptWallet for wallet not pertaining to current ledger, BadLedgerTxn on failure
        to send credential definition to ledger if need be, or IndyError for any other failure
        to create and store credential definition in wallet.

        :param s_id: schema identifier
        :param revocation: whether to support revocation for cred def
        :param rr_size: size of initial revocation registry (default as per _create_rev_reg()), if revocation supported
        :return: json credential definition as it appears on ledger
        """

        LOGGER.debug(
            'Issuer.send_cred_def >>> s_id: %s, revocation: %s, rr_size: %s',
            s_id, revocation, rr_size)

        rv_json = json.dumps({})
        schema_json = await self.get_schema(schema_key(s_id))
        schema = json.loads(schema_json)

        cd_id = cred_def_id(self.did, schema['seqNo'])
        private_key_ok = True
        with CRED_DEF_CACHE.lock:
            try:
                rv_json = await self.get_cred_def(cd_id)
                LOGGER.info(
                    'Cred def on schema %s version %s already exists on ledger; Issuer %s not sending another',
                    schema['name'], schema['version'], self.wallet.name)
            except AbsentCredDef:
                pass  # OK - about to create, store, and send it

            try:
                (
                    _, cred_def_json
                ) = await anoncreds.issuer_create_and_store_credential_def(
                    self.wallet.handle,
                    self.did,  # issuer DID
                    schema_json,
                    CD_ID_TAG,  # expect only one cred def per schema and issuer
                    'CL',
                    json.dumps({'support_revocation': revocation}))
                if json.loads(rv_json):
                    private_key_ok = False
                    LOGGER.warning(
                        'New cred def on %s in wallet shadows existing one on ledger: private key not usable',
                        cd_id)
                    # carry on though, this agent may have other roles so public key may be good enough
            except IndyError as x_indy:
                if x_indy.error_code == ErrorCode.AnoncredsCredDefAlreadyExistsError:
                    if json.loads(rv_json):
                        LOGGER.info(
                            'Issuer wallet %s reusing existing cred def on schema %s version %s',
                            self.wallet.name, schema['name'],
                            schema['version'])
                    else:
                        LOGGER.debug(
                            'Issuer.send_cred_def: <!< corrupt wallet %s',
                            self.wallet.name)
                        raise CorruptWallet(
                            'Corrupt Issuer wallet {} has cred def on schema {} version {} not on ledger'
                            .format(self.wallet.name, schema['name'],
                                    schema['version']))
                else:
                    LOGGER.debug(
                        'Issuer.send_cred_def: <!< cannot store cred def in wallet %s: indy error code %s',
                        self.wallet.name, x_indy.error_code)
                    raise

            if not json.loads(
                    rv_json
            ):  # checking the ledger returned no cred def: send it
                req_json = await ledger.build_cred_def_request(
                    self.did, cred_def_json)
                await self._sign_submit(req_json)
                rv_json = await self.get_cred_def(
                    cd_id)  # pick up from ledger and parse; add to cache

                if revocation:
                    await self._sync_revoc(
                        rev_reg_id(cd_id, 0),
                        rr_size)  # create new rev reg, tails file for tag 0

        if revocation and private_key_ok:
            for tag in [
                    str(t) for t in range(
                        int(Tails.next_tag(self._dir_tails, cd_id)[0]))
            ]:  # '0' to str(next-1)
                await self._sync_revoc(rev_reg_id(cd_id, tag),
                                       rr_size if tag == 0 else None)

        dir_cred_def = join(self._dir_tails, cd_id)
        if not isdir(
                dir_cred_def
        ):  # make sure a directory exists for box id collection when required, revo or not
            makedirs(dir_cred_def, exist_ok=True)

        LOGGER.debug('Issuer.send_cred_def <<< %s', rv_json)
        return rv_json