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 <<<')
def current_rev_reg_id(base_dir: str, cd_id: str) -> str: """ Return the current revocation registry identifier for input credential definition identifier, in input directory. Raise AbsentTails if no corresponding tails file, signifying no such revocation registry defined. :param base_dir: base directory for tails files, thereafter split by cred def id :param cd_id: credential definition identifier of interest :return: identifier for current revocation registry on input credential definition identifier """ LOGGER.debug('Tails.current_rev_reg_id >>> base_dir: %s, cd_id: %s', base_dir, cd_id) if not ok_cred_def_id(cd_id): LOGGER.debug('Tails.current_rev_reg_id <!< Bad cred def id %s', cd_id) raise BadIdentifier('Bad cred def id {}'.format(cd_id)) tags = [ int(rev_reg_id2tag(basename(f))) for f in Tails.links(base_dir) if cd_id in basename(f) ] if not tags: raise AbsentTails( 'No tails files present for cred def id {}'.format(cd_id)) rv = rev_reg_id(cd_id, str(max(tags))) # ensure 10 > 9, not '9' > '10' LOGGER.debug('Tails.current_rev_reg_id <<< %s', rv) return rv
def __init__(self, base_dir: str, cd_id: str, tag: str = None): """ Initialize programmatic association between revocation registry identifier (on credential definition on input identifier plus tag, default most recent), and tails file, via symbolic link. Raise AbsentTails if (rev reg id) symbolic link or (tails hash) tails file not present. :param base_dir: base directory for tails files, thereafter split by cred def id :param cd_id: credential definition identifier of interest :param tag: revocation registry identifier tag of interest, default to most recent """ LOGGER.debug('Issuer.__init__ >>> base_dir: %s, cd_id: %s, tag: %s', base_dir, cd_id, tag) if not ok_cred_def_id(cd_id): LOGGER.debug('Tails.__init__ <!< Bad cred def id %s', cd_id) raise BadIdentifier('Bad cred def id {}'.format(cd_id)) if tag is None: self._rr_id = Tails.current_rev_reg_id(base_dir, cd_id) else: # including tag == 0 self._rr_id = rev_reg_id(cd_id, tag) if self._rr_id not in [basename(f) for f in Tails.links(base_dir)]: LOGGER.debug( 'Tails.__init__ <!< No tails file present for cred def id %s on rev reg id tag %s', cd_id, tag) raise AbsentTails( 'No tails file present for cred def id {} on rev reg id tag {}' .format(cd_id, tag)) path_link = join(Tails.dir(base_dir, self._rr_id), self._rr_id) if not islink(path_link): raise AbsentTails( 'No symbolic link present at {} for rev reg id {}'.format( path_link, self._rr_id)) path_tails = Tails.linked(base_dir, self._rr_id) if not isfile(path_tails): raise AbsentTails( 'No tails file present at {} for rev reg id {}'.format( path_tails, self._rr_id)) self._tails_config_json = json.dumps({ 'base_dir': dirname(path_tails), 'uri_pattern': '', 'file': basename(path_tails) }) self._reader_handle = None LOGGER.debug('Tails.__init__ <<<')
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 <<<')
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'] if not ok_cred_def_id(cd_id): LOGGER.debug('Issuer.create_cred <!< Bad cred def id %s', cd_id) raise BadIdentifier('Bad cred def id {}'.format(cd_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 }), 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, rr_id, 'CL_ACCUM', rr_delta_json) await self._sign_submit(rre_req_json) assert rr_id == tails.rr_id resp_json = await self._sign_submit(rre_req_json) resp = json.loads(resp_json) rv = (cred_json, cred_revoc_id, self.pool.protocol.txn2epoch(resp)) 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 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) if not ok_schema_id(s_id): LOGGER.debug('Issuer.send_cred_def <!< Bad schema id %s', s_id) raise BadIdentifier('Bad schema id {}'.format(s_id)) 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'], self.pool.protocol) 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, self.pool.protocol.cd_id_tag( False ), # 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 anchor 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) for _ in range(16): # reasonable timeout try: rv_json = await self.get_cred_def(cd_id ) # adds to cache break except AbsentCredDef: await sleep(1) LOGGER.info( 'Sent cred def %s to ledger, waiting 1s for its appearance', cd_id) if not rv_json: LOGGER.debug( 'Issuer.send_cred_def <!< timed out waiting on sent cred_def %s', cd_id) raise BadLedgerTxn( 'Timed out waiting on sent cred_def {}'.format(cd_id)) 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) makedirs(join(self._dir_tails, cd_id), exist_ok=True ) # make sure dir exists for box id collection, revo or not LOGGER.debug('Issuer.send_cred_def <<< %s', rv_json) return rv_json
async def create_cred(self, cred_offer_json, cred_req_json: str, cred_attrs: dict, rr_size: int = None) -> (str, str): """ 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. Raise WalletState for closed wallet. 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 64 and doubling iteratively through a maximum of 100000) and the revocation registry builder posture (see RevRegBuilder.__init__()), this operation may delay credential creation by several seconds. The use of an external revocation registry builder runs a parallel process, skirting this delay, but is more costly at initialization. :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 original 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 RevRegBuilder.create_rev_reg()) if necessary :return: tuple with newly issued credential json, credential revocation identifier (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) if not self.wallet.handle: LOGGER.debug('Issuer.create_cred <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) cd_id = json.loads(cred_offer_json)['cred_def_id'] if not ok_cred_def_id(cd_id): LOGGER.debug('Issuer.create_cred <!< Bad cred def id %s', cd_id) raise BadIdentifier('Bad cred def id {}'.format(cd_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_for_issue() sets this index in revo cache try: ( cred_json, cred_revoc_id, _ ) = await anoncreds.issuer_create_credential( # issue by default to rr self.wallet.handle, cred_offer_json, cred_req_json, json.dumps({ k: cred_attr_value(cred_attrs[k]) for k in cred_attrs }), rr_id, tails.reader_handle) rv = (cred_json, cred_revoc_id) 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) if self.rrbx: await self._set_rev_reg(rr_id, rr_size) else: await self.rrb.create_rev_reg( rr_id, rr_size or rr_size_suggested) await self._send_rev_reg_def(rr_id) REVO_CACHE[rr_id].tails = await Tails( self.dir_tails, cd_id).open() # symlink OK now return await self.create_cred(cred_offer_json, cred_req_json, cred_attrs) 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, None) 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, revo: 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, WalletState for closed wallet, or IndyError for any other failure to create and store credential definition in wallet. :param s_id: schema identifier :param revo: whether to support revocation for cred def :param rr_size: size of initial revocation registry (default as per RevRegBuilder.create_rev_reg()), if revocation supported :return: json credential definition as it appears on ledger """ LOGGER.debug( 'Issuer.send_cred_def >>> s_id: %s, revo: %s, rr_size: %s', s_id, revo, rr_size) if not ok_schema_id(s_id): LOGGER.debug('Issuer.send_cred_def <!< Bad schema id %s', s_id) raise BadIdentifier('Bad schema id {}'.format(s_id)) if not self.wallet.handle: LOGGER.debug('Issuer.send_cred_def <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) if not self.pool: LOGGER.debug('Issuer.send_cred_def <!< issuer %s has no pool', self.name) raise AbsentPool( 'Issuer {} has no pool: cannot send cred def'.format( self.name)) 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'], self.pool.protocol) 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.name) except AbsentCredDef: pass # OK - about to create, store, and send it (cred_def_json, private_key_ok) = await self._create_cred_def( schema, json.loads(rv_json), revo) 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) for _ in range(16): # reasonable timeout try: rv_json = await self.get_cred_def(cd_id ) # adds to cache break except AbsentCredDef: await asyncio.sleep(1) LOGGER.info( 'Sent cred def %s to ledger, waiting 1s for its appearance', cd_id) if not rv_json: LOGGER.debug( 'Issuer.send_cred_def <!< timed out waiting on sent cred_def %s', cd_id) raise BadLedgerTxn( 'Timed out waiting on sent cred_def {}'.format(cd_id)) if revo: # create new rev reg for tag '0' if self.rrbx: (_, rr_size_suggested) = Tails.next_tag( self.dir_tails, cd_id) self.rrb.mark_in_progress(rev_reg_id(cd_id, '0'), rr_size or rr_size_suggested) await self._sync_revoc_for_issue(rev_reg_id( cd_id, '0'), rr_size) # sync rev reg on tag '0' if revo and private_key_ok: for tag in [ str(t) for t in range( 1, int(Tails.next_tag(self.dir_tails, cd_id)[0])) ]: # '1' to next-1 await self._sync_revoc_for_issue( rev_reg_id(cd_id, tag), rr_size if tag == '0' else None) makedirs( join(self.dir_tails, cd_id), exist_ok=True) # dir required for box id collection, revo or not LOGGER.debug('Issuer.send_cred_def <<< %s', rv_json) return rv_json