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 """ 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)]: raise AbsentTails('No tails files present for cred def id {} on 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_cfg_json = json.dumps({ 'base_dir': dirname(path_tails), 'uri_pattern': '', 'file': basename(path_tails) }) self._reader_handle = None
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 """ 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)) return rev_reg_id(cd_id, str(max(tags))) # ensure 10 > 9, not '9' > '10'
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