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 <<<')
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 <<<')
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 <<<')
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)
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)
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
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
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
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
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
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