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 _get_rev_reg_def(self, rr_id: str) -> str: """ Get revocation registry definition from ledger by its identifier. Raise AbsentRevReg for no such revocation registry, logging any error condition and raising BadLedgerTxn on bad request. Retrieve the revocation registry definition from the agent's revocation cache if it has it; cache it en passant if it does not (and such a revocation registry definition exists on the ledger). :param rr_id: (revocation registry) identifier string, of the format '<issuer-did>:4:<issuer-did>:3:CL:<schema-seq-no>:<tag>:CL_ACCUM:<tag>' :return: revocation registry definition json as retrieved from ledger """ LOGGER.debug('_BaseAgent._get_rev_reg_def >>> rr_id: %s', rr_id) rv_json = json.dumps({}) with REVO_CACHE.lock: revo_cache_entry = REVO_CACHE.get(rr_id, None) rr_def = revo_cache_entry.rev_reg_def if revo_cache_entry else None if rr_def: LOGGER.info( '_BaseAgent._get_rev_reg_def: rev reg def for %s from cache', rr_id) rv_json = json.dumps(rr_def) else: get_rrd_req_json = await ledger.build_get_revoc_reg_def_request( self.did, rr_id) resp_json = await self._submit(get_rrd_req_json) try: (_, rv_json) = await ledger.parse_get_revoc_reg_def_response( resp_json) rr_def = json.loads(rv_json) except IndyError: # ledger replied, but there is no such rev reg LOGGER.debug( '_BaseAgent._get_rev_reg_def: <!< no rev reg exists on %s', rr_id) raise AbsentRevReg('No rev reg exists on {}'.format(rr_id)) if revo_cache_entry is None: REVO_CACHE[rr_id] = RevoCacheEntry(rr_def, None) else: REVO_CACHE[rr_id].rev_reg_def = rr_def LOGGER.debug('_BaseAgent._get_rev_reg_def <<< %s', rv_json) return rv_json
async def _sync_revoc(self, rr_id: str, rr_size: int = None) -> None: """ Create revoc registry if need be for input revocation registry identifier; open and cache tails file reader. :param rr_id: revocation registry identifier :param rr_size: if new revocation registry necessary, its size (default as per _create_rev_reg()) """ LOGGER.debug('Issuer._sync_revoc >>> rr_id: %s, rr_size: %s', rr_id, rr_size) (cd_id, tag) = rev_reg_id2cred_def_id__tag(rr_id) try: await self.get_cred_def(cd_id) except AbsentCredDef: LOGGER.debug( 'Issuer._sync_revoc: <!< tails tree %s may be for another ledger; no cred def found on %s', self._dir_tails, cd_id) raise AbsentCredDef( 'Tails tree {} may be for another ledger; no cred def found on {}' .format(self._dir_tails, cd_id)) with REVO_CACHE.lock: revo_cache_entry = REVO_CACHE.get(rr_id, None) tails = None if revo_cache_entry is None else revo_cache_entry.tails if tails is None: # it's a new revocation registry, or not yet set in cache try: tails = await Tails(self._dir_tails, cd_id, tag).open() except AbsentTails: await self._create_rev_reg( rr_id, rr_size) # it's a new revocation registry tails = await Tails(self._dir_tails, cd_id, tag).open() # symlink should exist now if revo_cache_entry is None: REVO_CACHE[rr_id] = RevoCacheEntry(None, tails) else: REVO_CACHE[rr_id].tails = tails LOGGER.debug('Issuer._sync_revoc <<<')
async def load_cache(self, archive: bool = False) -> int: """ Load caches and archive enough to go offline and be able to verify proof on content marked of interest in configuration. Return timestamp (epoch seconds) of cache load event, also used as subdirectory for cache archives. :param archive: whether to archive caches to disk :return: cache load event timestamp (epoch seconds) """ LOGGER.debug('Verifier.load_cache >>> archive: %s', archive) rv = int(time()) for s_id in self.cfg.get('archive-on-close', {}).get('schema_id', {}): with SCHEMA_CACHE.lock: await self.get_schema(s_id) for cd_id in self.cfg.get('archive-on-close', {}).get('cred_def_id', {}): with CRED_DEF_CACHE.lock: await self.get_cred_def(cd_id) for rr_id in self.cfg.get('archive-on-close', {}).get('rev_reg_id', {}): await self._get_rev_reg_def(rr_id) with REVO_CACHE.lock: revo_cache_entry = REVO_CACHE.get(rr_id, None) if revo_cache_entry: try: await revo_cache_entry.get_state_json( self._build_rr_state_json, rv, rv) except ClosedPool: LOGGER.warning( 'Verifier %s is offline from pool %s, cannot update revo cache reg state for %s to %s', self.wallet.name, self.pool.name, rr_id, rv) if archive: Caches.archive(self.dir_cache) LOGGER.debug('Verifier.load_cache <<< %s', rv) return rv
async def load_cache(self, archive: bool = False) -> int: """ Load caches and archive enough to go offline and be able to generate proof on all credentials in wallet. Return timestamp (epoch seconds) of cache load event, also used as subdirectory for cache archives. :return: cache load event timestamp (epoch seconds) """ LOGGER.debug('HolderProver.load_cache >>> archive: %s', archive) rv = int(time()) box_ids = json.loads(await self.get_box_ids_json()) for s_id in box_ids['schema_id']: with SCHEMA_CACHE.lock: await self.get_schema(s_id) for cd_id in box_ids['cred_def_id']: with CRED_DEF_CACHE.lock: await self.get_cred_def(cd_id) for rr_id in box_ids['rev_reg_id']: await self._get_rev_reg_def(rr_id) with REVO_CACHE.lock: revo_cache_entry = REVO_CACHE.get(rr_id, None) if revo_cache_entry: try: await revo_cache_entry.get_delta_json( self._build_rr_delta_json, rv, rv) except ClosedPool: LOGGER.warning( 'Holder-Prover %s is offline from pool %s, cannot update revo cache reg delta for %s to %s', self.wallet.name, self.pool.name, rr_id, rv) if archive: Caches.archive(self.dir_cache) LOGGER.debug('HolderProver.load_cache <<< %s', rv) return rv
async def verify_proof(self, proof_req: dict, proof: dict) -> str: """ Verify proof as Verifier. Raise AbsentRevReg if a proof cites a revocation registry that does not exist on the distributed ledger. :param proof_req: proof request as Verifier creates, as per proof_req_json above :param proof: proof as HolderProver creates :return: json encoded True if proof is valid; False if not """ LOGGER.debug('Verifier.verify_proof >>> proof_req: %s, proof: %s', proof_req, proof) s_id2schema = {} cd_id2cred_def = {} rr_id2rr_def = {} rr_id2rr = {} proof_ids = proof['identifiers'] for proof_id in proof_ids: # schema s_id = proof_id['schema_id'] if s_id not in s_id2schema: schema = json.loads( await self.get_schema(s_id)) # add to cache en passant if not schema: LOGGER.debug( 'Verifier.verify_proof: <!< absent schema %s, proof req may be for another ledger', s_id) raise AbsentSchema( 'Absent schema {}, proof req may be for another ledger' .format(s_id)) s_id2schema[s_id] = schema # cred def cd_id = proof_id['cred_def_id'] if cd_id not in cd_id2cred_def: cred_def = json.loads( await self.get_cred_def(cd_id)) # add to cache en passant cd_id2cred_def[cd_id] = cred_def # rev reg def rr_id = proof_id['rev_reg_id'] if not rr_id: continue rr_def_json = await self._get_rev_reg_def(rr_id) rr_id2rr_def[rr_id] = json.loads(rr_def_json) # timestamp timestamp = proof_id['timestamp'] with REVO_CACHE.lock: revo_cache_entry = REVO_CACHE.get(rr_id, None) (rr_json, _) = await revo_cache_entry.get_state_json( self._build_rr_state_json, timestamp, timestamp) if rr_id not in rr_id2rr: rr_id2rr[rr_id] = {} rr_id2rr[rr_id][timestamp] = json.loads(rr_json) rv = json.dumps(await anoncreds.verifier_verify_proof( json.dumps(proof_req), json.dumps(proof), json.dumps(s_id2schema), json.dumps(cd_id2cred_def), json.dumps(rr_id2rr_def), json.dumps(rr_id2rr))) LOGGER.debug('Verifier.verify_proof <<< %s', rv) return rv
async def build_proof_req_json(self, cd_id2spec: dict, cache_only: bool = False) -> str: """ Build and return indy-sdk proof request for input attributes and timestamps by cred def id. Raise AbsentInterval if caller specifies cache_only and default non-revocation intervals, but revocation cache does not have delta frames for any revocation registries on a specified cred def. :param cd_id2spec: dict mapping cred def ids to: - (optionally) 'attrs': lists of names of attributes of interest (omit for all, empty list or None for none) - (optionally) 'minima': (pred) integer lower-bounds of interest (omit, empty list, or None for none) - (optionally), 'interval': (2-tuple) pair of epoch second counts marking 'from' and 'to' timestamps, or single epoch second count to set 'from' and 'to' the same: default (now, now) if cache_only is clear, or latest values from cache if cache_only is set. e.g., :: { 'Vx4E82R17q...:3:CL:16:0': { 'attrs': [ # request attrs 'name' and 'favouriteDrink' from this cred def's schema 'name', 'favouriteDrink' ], 'minima': { # request predicate score>=80 from this cred def 'score': 80 } 'interval': 1528116008 # same instant for all attrs and preds of corresponding schema }, 'R17v42T4pk...:3:CL:19:0': None, # request all attrs, no preds, default intervals on all attrs 'e3vc5K168n...:3:CL:23:0': {}, # request all attrs, no preds, default intervals on all attrs 'Z9ccax812j...:3:CL:27:0': { # request all attrs, no preds, this interval on all attrs 'interval': (1528112408, 1528116008) }, '9cHbp54C8n...:3:CL:37:0': { # request no attrs, one pred, specify interval on pred 'attrs': [], # or equivalently, 'attrs': None 'minima': { 'employees': '50' # nicety: implementation converts to int for caller }, 'interval': (1528029608, 1528116008) }, '6caBcmLi33...:3:CL:41:0': { # all attrs, one pred, default intervals to now on attrs & pred 'minima': { 'regEpoch': 1514782800 } } ... } :param cache_only: (True) take default intervals (per cred def id) from latest cached deltas, or (default False) use current time :return: indy-sdk proof request json """ LOGGER.debug( 'HolderProver.build_proof_req_json >>> cd_id2spec: %s, cache_only: %s', cd_id2spec, cache_only) cd_id2schema = {} now = int(time()) proof_req = { 'nonce': str(int(time())), 'name': 'proof_req', 'version': '0.0', 'requested_attributes': {}, 'requested_predicates': {} } for cd_id in cd_id2spec: interval = None cred_def = json.loads(await self.get_cred_def(cd_id)) seq_no = cred_def_id2seq_no(cd_id) cd_id2schema[cd_id] = json.loads(await self.get_schema(seq_no)) if 'revocation' in cred_def['value']: if cache_only and not (cd_id2spec.get(cd_id, {}) or {}).get( 'interval', None): with REVO_CACHE.lock: (fro, to) = REVO_CACHE.dflt_interval(cd_id) if not (fro and to): LOGGER.debug( 'HolderProver.build_proof_req_json: <!< no cached delta for non-revoc interval on %s', cd_id) raise AbsentInterval( 'No cached delta for non-revoc interval on {}'. format(cd_id)) interval = {'from': fro, 'to': to} else: fro_to = cd_id2spec[cd_id].get( 'interval', (now, now)) if cd_id2spec[cd_id] else (now, now) interval = { 'from': fro_to if isinstance(fro_to, int) else min(fro_to), 'to': fro_to if isinstance(fro_to, int) else max(fro_to) } for attr in (cd_id2spec[cd_id].get( 'attrs', cd_id2schema[cd_id]['attrNames']) or [] if cd_id2spec[cd_id] else cd_id2schema[cd_id]['attrNames']): attr_uuid = '{}_{}_uuid'.format(seq_no, attr) proof_req['requested_attributes'][attr_uuid] = { 'name': attr, 'restrictions': [{ 'cred_def_id': cd_id }] } if interval: proof_req['requested_attributes'][attr_uuid][ 'non_revoked'] = interval for attr in (cd_id2spec[cd_id].get('minima', {}) or {} if cd_id2spec[cd_id] else {}): pred_uuid = '{}_{}_uuid'.format(seq_no, attr) try: proof_req['requested_predicates'][pred_uuid] = { 'name': attr, 'p_type': '>=', 'p_value': int(cd_id2spec[cd_id]['minima'][attr]), 'restrictions': [{ 'cred_def_id': cd_id }] } except ValueError: LOGGER.info( 'cannot build predicate on non-int minimum %s for %s', cd_id2spec[cd_id]['minima'][attr], attr) continue # int conversion failed - reject candidate if interval: proof_req['requested_predicates'][pred_uuid][ 'non_revoked'] = interval rv_json = json.dumps(proof_req) LOGGER.debug('HolderProver.build_proof_req_json <<< %s', rv_json) return rv_json
async def create_proof(self, proof_req: dict, creds: dict, requested_creds: dict) -> str: """ Create proof as HolderProver. Raise: * AbsentLinkSecret if link secret not set * CredentialFocus on attempt to create proof on no creds or multiple creds for a credential definition * AbsentTails if missing required tails file * BadRevStateTime if a timestamp for a revocation registry state in the proof request occurs before revocation registry creation * IndyError for any other indy-sdk error. * AbsentInterval if creds missing non-revocation interval, but cred def supports revocation :param proof_req: proof request as per get_creds() above :param creds: credentials to prove :param requested_creds: data structure with self-attested attribute info, requested attribute info and requested predicate info, assembled from get_creds() and filtered for content of interest. I.e., :: { 'self_attested_attributes': {}, 'requested_attributes': { 'attr0_uuid': { 'cred_id': string, 'timestamp': integer, # for revocation state 'revealed': bool }, ... }, 'requested_predicates': { 'predicate0_uuid': { 'cred_id': string, 'timestamp': integer # for revocation state } } } :return: proof json """ LOGGER.debug( 'HolderProver.create_proof >>> proof_req: %s, creds: %s, requested_creds: %s', proof_req, creds, requested_creds) self._assert_link_secret('create_proof') x_uuids = [ attr_uuid for attr_uuid in creds['attrs'] if len(creds['attrs'][attr_uuid]) != 1 ] if x_uuids: LOGGER.debug( 'HolderProver.create_proof: <!< creds specification out of focus (non-uniqueness)' ) raise CredentialFocus( 'Proof request requires unique cred per attribute; violators: {}' .format(x_uuids)) s_id2schema = {} # schema identifier to schema cd_id2cred_def = { } # credential definition identifier to credential definition rr_id2timestamp = { } # revocation registry of interest to timestamp of interest (or None) rr_id2cr_id = { } # revocation registry of interest to credential revocation identifier for referents in {**creds['attrs'], **creds['predicates']}.values(): interval = referents[0].get('interval', None) cred_info = referents[0]['cred_info'] s_id = cred_info['schema_id'] if s_id not in s_id2schema: schema = json.loads( await self.get_schema(s_id)) # add to cache en passant if not schema: LOGGER.debug( 'HolderProver.create_proof: <!< absent schema %s, proof req may be for another ledger', s_id) raise AbsentSchema( 'Absent schema {}, proof req may be for another ledger' .format(s_id)) s_id2schema[s_id] = schema cd_id = cred_info['cred_def_id'] if cd_id not in cd_id2cred_def: cred_def = json.loads( await self.get_cred_def(cd_id)) # add to cache en passant cd_id2cred_def[cd_id] = cred_def rr_id = cred_info['rev_reg_id'] if rr_id: await self._sync_revoc( rr_id) # link tails file to its rr_id if it's new if interval: if rr_id not in rr_id2timestamp: if interval['to'] > int(time()): LOGGER.debug( 'HolderProver.create_proof: <!< interval to %s for rev reg %s is in the future', interval['to'], rr_id) raise BadRevStateTime( 'Revocation registry {} timestamp {} is in the future' .format(rr_id, interval['to'])) rr_id2timestamp[rr_id] = interval['to'] elif 'revocation' in cd_id2cred_def[cd_id]['value']: LOGGER.debug( 'HolderProver.create_proof: <!< creds on cred def id %s missing non-revocation interval', cd_id) raise AbsentInterval( 'Creds on cred def id {} missing non-revocation interval' .format(cd_id)) if rr_id in rr_id2cr_id: continue rr_id2cr_id[rr_id] = cred_info['cred_rev_id'] rr_id2rev_state = {} # revocation registry identifier to its state with REVO_CACHE.lock: for rr_id in rr_id2timestamp: revo_cache_entry = REVO_CACHE.get(rr_id, None) tails = revo_cache_entry.tails if revo_cache_entry else None if tails is None: # missing tails file LOGGER.debug( 'HolderProver.create_proof: <!< missing tails file for rev reg id %s', rr_id) raise AbsentTails( 'Missing tails file for rev reg id {}'.format(rr_id)) rr_def_json = await self._get_rev_reg_def(rr_id) (rr_delta_json, ledger_timestamp) = await revo_cache_entry.get_delta_json( self._build_rr_delta_json, rr_id2timestamp[rr_id], rr_id2timestamp[rr_id]) rr_state_json = await anoncreds.create_revocation_state( tails.reader_handle, rr_def_json, rr_delta_json, ledger_timestamp, rr_id2cr_id[rr_id]) rr_id2rev_state[rr_id] = { rr_id2timestamp[rr_id]: json.loads(rr_state_json) } rv = await anoncreds.prover_create_proof(self.wallet.handle, json.dumps(proof_req), json.dumps(requested_creds), self._link_secret, json.dumps(s_id2schema), json.dumps(cd_id2cred_def), json.dumps(rr_id2rev_state)) LOGGER.debug('HolderProver.create_proof <<< %s', rv) return rv