diff --git a/aries_cloudagent/anoncreds/holder.py b/aries_cloudagent/anoncreds/holder.py index 961ebe260f..41495fd7e8 100644 --- a/aries_cloudagent/anoncreds/holder.py +++ b/aries_cloudagent/anoncreds/holder.py @@ -4,16 +4,16 @@ import json import logging import re -from typing import Dict, Optional, Sequence, Tuple, Union import uuid +from typing import Dict, Optional, Sequence, Tuple, Union from anoncreds import ( AnoncredsError, Credential, CredentialRequest, CredentialRevocationState, - PresentCredentials, Presentation, + PresentCredentials, ) from anoncreds.bindings import create_link_secret from aries_askar import AskarError, AskarErrorCode @@ -57,6 +57,7 @@ class AnonCredsHolder: """AnonCreds holder class.""" MASTER_SECRET_ID = "default" + RECORD_TYPE_MIME_TYPES = "attribute-mime-types" def __init__(self, profile: Profile): """Initialize an AnonCredsHolder instance. @@ -409,6 +410,7 @@ async def credential_revoked( rev_reg_id = cred.rev_reg_id # TODO Use anoncreds registry + # check if cred.rev_reg_id is returning None or 'None' if rev_reg_id: cred_rev_id = cred.rev_reg_index (rev_reg_delta, _) = await ledger.get_revoc_reg_delta( diff --git a/aries_cloudagent/anoncreds/tests/mock_objects.py b/aries_cloudagent/anoncreds/tests/mock_objects.py new file mode 100644 index 0000000000..8398dd4494 --- /dev/null +++ b/aries_cloudagent/anoncreds/tests/mock_objects.py @@ -0,0 +1,238 @@ +MOCK_PRES_REQ = { + "nonce": "182453895158932070575246", + "name": "Step 3 Send your Health Information", + "version": "1.0", + "requested_attributes": { + "biomarker_attrs_0": { + "names": [ + "name", + "concentration", + "unit", + "range", + "collected_on", + "biomarker_id", + "researcher_share", + ], + "restrictions": [ + {"schema_name": "MYCO Biomarker", "attr::name::value": "Iron"} + ], + }, + "consent_attrs": { + "restrictions": [ + { + "schema_name": "MYCO Consent Enablement", + "schema_version": "0.1.0", + "attr::jti_unique_identifier::value": "205b1ea0-7848-48d4-b52b-339122d84f62", + } + ], + "name": "jti_unique_identifier", + }, + }, + "requested_predicates": {}, +} + +MOCK_PRES = { + "proof": { + "proofs": [ + { + "primary_proof": { + "eq_proof": { + "revealed_attrs": { + "biomarker_id": "33034450023603237719386825060766757598085121996569112944281451290292212516012", + "collected_on": "92231735610070911075924224447204218356256133056723930517696107260511721601349", + "concentration": "10", + "name": "85547618788485118809771015708850341281587970912661276233439574555663751388073", + "range": "106828626115908025842177441696860557581575579893927923198365300598359723920768", + "researcher_share": "101264834079306301897660576123112461042861436742738894013248454492965796383403", + "unit": "38351211041892038382023569421847544683371072212679556578649761181279472893849", + }, + "a_prime": "80156520245352052628208149565161465200964633377479145197038408116901327106468493831807000641577246417448908134495822028339761705905365398613527463662816881507291787145610182891716009505407072490691097943029471835157968113065071523597746984296197661560454442163361095634052138951650373193896962906203169809352467024247772052836999799731422581068645748537557874869718897034120634529002420631012358510111427944993245065350416694516913472010105229188198167306183788520891926236449848811955646933539477960935319919207451858981065765523367984374104834278565184338252025155136368869580505679884921590811310587077071610172673", + "e": "115602723672843258810892161808995599340414281260248127600708536325470178701996999306086286379312077726886107268519700961209712187789855371", + "v": "1250383260306407741656763352595256748825474237767244783206569756476708112785930898966696687140808011529311298553822794830872826226191807175199015541611342880032928005827271961840046208463350458298210749103878893742434685172894883857423865293195542824393317226300133796527531436931435189766065404966370796699897584860421160155369018136946091524266742514828667575397735892093187106092545876795688095293610064164136737808333322708435913545499149948994191514980395955519036106660001526586674248282052492138917987323789012051794441548696998993861159018178474063785171288325900474499496141522583368982451169653258746506425495702762445790848698570457196767532483566475068200091609719957656394696938689265240025099424248587121592521826940348286940172887963179718337593603053496022182071613592070622825622277436966372346642772481567879001423472517233061740522533372490151585309457871632521280719357505751796940152868034526426510835", + "m": { + "master_secret": "3455871040557234123393960708120725061759594951341120214330342075748561632734634451036095543889895409812764789858455375956895105746442946098665140470124325622343440794421325163223", + "client_share": "4233663763294709836704307308997831519311512039775169744174375585917035614714239153287862168426091336550799195245481707264207548331415960277065672755643752404180562900805493953484", + }, + "m2": "12942698897200869280316481431207639453433287089474860040781378232999349549981799159560238504559317917040820580596635945161264308025301203452846862479261473387068350544024561412435", + }, + "ge_proofs": [], + } + }, + { + "primary_proof": { + "eq_proof": { + "revealed_attrs": { + "jti_unique_identifier": "46414468020333259158238797309781111434265856695713363124410805958145233348633" + }, + "a_prime": "52825780315318905340996188008133401356826233601375100674436798295026172087388431332751168238882607201020021795967828258295811342078457860422414605408183505911891895360825745994390769724939582542658347473498091021796952186290990181881158576706521445646669342676592451422000320708168877298354804819261007033664223006892049856834172427934815827786052257552492013807885418893279908149441273603109213847535482251568996326545234910687135167595657148526602160452192374611721411569543183642580629352619161783646990187905911781524203367796090408992624211661598980626941053749241077719601278347846928693650092940416717449494816", + "e": "40342480172543061520030194979861449480343743039487113094246205723322643070249538229638327935935486373873622430409109409257546971244601965", + "v": "217871997575635857881367472262154388060800564043554848081521162883333745687724235201324121915821236796357195214089699645741515836727882126142579489701412861659136426497703162695983681701205672924385915403141611021784136285588350763399255203187442277784718461565122805239422370067600654500115262174706580098147603414365915243447789285877195068031630371954678432401446457453517813298670236942253026249413255471803997869331683293818651006043399070308083119054618677128448043841313844695654424369871669436628257531643623230026240200330490039607166147891705813033761093730859310423856156850596341547950105490585959768382544221555877471751940512766452511773683786023245283041103270102119125303027835868565240336923422734962345750992898991606841120358203160628015844345063465293475128118937815965000466081345494616126511595974927544434058100817176268040385848789013718618727873445834393897904247054897801708217939187593785671914", + "m": { + "iat_consent_timestamp": "7919242808448912829024078929834347184203169048480606699350973804205285806978474375691141504249426249676222104091995582731720654507393708298132400435805626192291975477967402460279", + "master_secret": "3455871040557234123393960708120725061759594951341120214330342075748561632734634451036095543889895409812764789858455375956895105746442946098665140470124325622343440794421325163223", + "data_controller": "16070549690575784944224634793654539357398383214512772967411296056738507137421264813779497172425030465490587794790393434847583852932544021088761347641812155158324233253206392974293", + "notice": "2790610958721083178459621377821800672322230987466716467063649577108407884592339521820875278264969393963213925568888672412150769438560815981777952572004955362915245795447078373509", + "sensitive": "13552814315985495030467505807226704038231487014593307078913973520081443107274508887651839292151852713782653522711975492131914644109941607616672243509214979259100892541150351227883", + "services": "14860984314279608355643170908802532226194914773406547259519961082467311361623076451869406343140860447342041426195737612897540117192702117380288330928866665314831926780606136352645", + "sub_subject_identifier": "11736177517163751882849070942823049196298287414132249166618760803125435466270948777194044507635346721244111946358927525083691171695431736819244809221351813271261283779276670885101", + "moc_method_of_collection": "10026360820367693771310999595495505533281326977349798360729122862705999157070660881611421445424239119786180921960380892002204780026072600494332540208429642332890963846523547470729", + "jurisdiction_data_processing": "15829143141425514118932461858094583045441924952665872659029333578019676797278419825311275014912077620757631693167948665554731430154156737419706553672424812320891308795411687679270", + "iss_internet_processing_uri": "6900796243066434651671715348976599009606292569990892886896520779618011026060325075822786686418461731663661832508437549373109822105600719490952253743950241384782222356411498407620", + "version_consent_specification": "7796257942256624260327966366702213561879098947042014532961291550019706546662478888172243088973621029223408695289700984802154645011280488167967047321149956253054269901250137513345", + "policy_url": "12241676508867847022708464707584814145889660003604359058532137895063826021524887759921830911553663255421852525705197991376264187781979066233701110706958983099645275940668404311601", + }, + "m2": "6509130065158989037891281073557909501783443634141673890142284302459280804904096303151728187237486991775852971807701594247754409108836089746736345158069365449802597798950172729241", + }, + "ge_proofs": [], + } + }, + ], + "aggregated_proof": { + "c_hash": "81763443376178433216866153835042672285397553441148068557996780431098922863180", + "c_list": [ + [2, 122, 246, 66, 85, 35, 17, 213, 1], + [1, 162, 117, 246, 95, 154, 129, 32], + ], + }, + }, + "requested_proof": { + "revealed_attrs": { + "consent_attrs": { + "sub_proof_index": 1, + "raw": "205b1ea0-7848-48d4-b52b-339122d84f62", + "encoded": "46414468020333259158238797309781111434265856695713363124410805958145233348633", + } + }, + "revealed_attr_groups": { + "biomarker_attrs_0": { + "sub_proof_index": 0, + "values": { + "researcher_share": { + "raw": "bf712cb328a92862b57f0dc806dec12a", + "encoded": "101264834079306301897660576123112461042861436742738894013248454492965796383403", + }, + "unit": { + "raw": "μM", + "encoded": "38351211041892038382023569421847544683371072212679556578649761181279472893849", + }, + "concentration": {"raw": "10", "encoded": "10"}, + "name": { + "raw": "Iron", + "encoded": "85547618788485118809771015708850341281587970912661276233439574555663751388073", + }, + "range": { + "raw": "9.00-30.0", + "encoded": "106828626115908025842177441696860557581575579893927923198365300598359723920768", + }, + "collected_on": { + "raw": "2020-07-05", + "encoded": "92231735610070911075924224447204218356256133056723930517696107260511721601349", + }, + "biomarker_id": { + "raw": "c9ace7dc-0485-4f3f-b466-16a27a80acf1", + "encoded": "33034450023603237719386825060766757598085121996569112944281451290292212516012", + }, + }, + } + }, + "self_attested_attrs": {}, + "unrevealed_attrs": {}, + "predicates": {}, + }, + "identifiers": [ + { + "schema_id": "CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3", + "cred_def_id": "CsQY9MGeD3CQP4EyuVFo5m:3:CL:14951:MYCO_Biomarker", + }, + { + "schema_id": "FbozHyf7j5q7TDn2s8MXZN:2:MYCO Consent Enablement:0.1.0", + "cred_def_id": "TUku9MDGa7QALbAJX4oAww:3:CL:531757:MYCO_Consent_Enablement", + }, + ], +} + +MOCK_SCHEMA = { + "issuerId": "https://example.org/issuers/74acabe2-0edc-415e-ad3d-c259bac04c15", + "name": "Example schema", + "version": "0.0.1", + "attrNames": ["name", "age", "vmax"], +} + +MOCK_CRED_DEF = { + "issuerId": "did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + "schemaId": "did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ/anoncreds/v0/SCHEMA/MemberPass/1.0", + "type": "CL", + "tag": "latest", + "value": { + "primary": { + "n": "779...397", + "r": { + "birthdate": "294...298", + "birthlocation": "533...284", + "citizenship": "894...102", + "expiry_date": "650...011", + "facephoto": "870...274", + "firstname": "656...226", + "link_secret": "521...922", + "name": "410...200", + "uuid": "226...757", + }, + "rctxt": "774...977", + "s": "750..893", + "z": "632...005", + }, + "revocation": { + "g": "1 154...813 1 11C...D0D 2 095..8A8", + "g_dash": "1 1F0...000", + "h": "1 131...8A8", + "h0": "1 1AF...8A8", + "h1": "1 242...8A8", + "h2": "1 072...8A8", + "h_cap": "1 196...000", + "htilde": "1 1D5...8A8", + "pk": "1 0E7...8A8", + "u": "1 18E...000", + "y": "1 068...000", + }, + }, +} + + +MOCK_SCHEMAS = { + "CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3": {"value": {}}, + "FbozHyf7j5q7TDn2s8MXZN:2:MYCO Consent Enablement:0.1.0": {"value": {}}, +} + +MOCK_CRED_DEFS = { + "CsQY9MGeD3CQP4EyuVFo5m:3:CL:14951:MYCO_Biomarker": {"value": {}}, + "TUku9MDGa7QALbAJX4oAww:3:CL:531757:MYCO_Consent_Enablement": {"value": {}}, +} + +MOCK_REV_REG_DEFS = { + "TUku9MDGa7QALbAJX4oAww:3:TUku9MDGa7QALbAJX4oAww:3:CL:18:tag:CL_ACCUM:0": { + "txnTime": 1500000000 + } +} + +MOCK_CRED = { + "schema_id": "Sc886XPwD1gDcHwmmLDeR2:2:degree schema:45.101.94", + "cred_def_id": "Sc886XPwD1gDcHwmmLDeR2:3:CL:229975:faber.agent.degree_schema", + "rev_reg_id": None, + "values": { + "first_name": {"raw": "Alice", "encoded": "113...335"}, + "last_name": {"raw": "Garcia", "encoded": "532...452"}, + "birthdate_dateint": {"raw": "19981119", "encoded": "19981119"}, + }, + "signature": { + "p_credential": { + "m_2": "992...312", + "a": "548...252", + "e": "259...199", + "v": "977...597", + }, + "r_credential": None, + }, + "signature_correctness_proof": {"se": "898...935", "c": "935...598"}, + "rev_reg": None, + "witness": None, +} diff --git a/aries_cloudagent/anoncreds/tests/test_holder.py b/aries_cloudagent/anoncreds/tests/test_holder.py index d32aed6056..c9acf891b5 100644 --- a/aries_cloudagent/anoncreds/tests/test_holder.py +++ b/aries_cloudagent/anoncreds/tests/test_holder.py @@ -1,8 +1,656 @@ -""" -Unit tests for the anoncreds holder: +import json +from copy import deepcopy -- receive and store credentials -- create presentation +import anoncreds +import pytest +from anoncreds import ( + AnoncredsError, + AnoncredsErrorCode, + Credential, + CredentialDefinition, + CredentialRequest, + CredentialRevocationState, + Presentation, + PresentCredentials, + RevocationRegistryDefinition, + Schema, +) +from aries_askar import AskarError, AskarErrorCode +from asynctest import TestCase -See tests under aries_cloudagent/indy/sdk/tests -""" +from aries_cloudagent.anoncreds.holder import AnonCredsHolder, AnonCredsHolderError +from aries_cloudagent.anoncreds.tests.mock_objects import ( + MOCK_CRED, + MOCK_CRED_DEF, + MOCK_PRES, + MOCK_PRES_REQ, +) +from aries_cloudagent.askar.profile_anon import AskarAnoncredsProfile +from aries_cloudagent.core.in_memory.profile import ( + InMemoryProfile, + InMemoryProfileSession, +) +from aries_cloudagent.indy.sdk.profile import IndySdkProfile +from aries_cloudagent.tests import mock +from aries_cloudagent.wallet.error import WalletNotFoundError + +from .. import holder as test_module +from ..models.anoncreds_cred_def import CredDef, CredDefValue, CredDefValuePrimary + + +class MockCredReceived: + def __init__(self, bad_schema=False, bad_cred_def=False): + self.schema_id = "Sc886XPwD1gDcHwmmLDeR2:2:degree schema:45.101.94" + self.cred_def_id = ( + "Sc886XPwD1gDcHwmmLDeR2:3:CL:229975:faber.agent.degree_schema" + ) + + if bad_schema: + self.schema_id = "bad-schema-id" + if bad_cred_def: + self.cred_def_id = "bad-cred-def-id" + + schema_id = "Sc886XPwD1gDcHwmmLDeR2:2:degree schema:45.101.94" + cred_def_id = "Sc886XPwD1gDcHwmmLDeR2:3:CL:229975:faber.agent.degree_schema" + rev_reg_id = None + + def to_json_buffer(self): + return b"credential" + + +class MockCredential: + def __init__(self, bad_schema=False, bad_cred_def=False): + self.bad_schema = bad_schema + self.bad_cred_def = bad_cred_def + + cred = mock.AsyncMock(auto_spec=Credential) + + def to_dict(self): + return MOCK_CRED + + def process(self, *args, **kwargs): + return MockCredReceived(self.bad_schema, self.bad_cred_def) + + +class MockMasterSecret: + value = b"master-secret" + + +class MockCredEntry: + def __init__(self, rev_reg=False) -> None: + mock_cred = deepcopy(MOCK_CRED) + if rev_reg: + mock_cred["rev_reg_id"] = "rev-reg-id" + self.name = "name" + self.value = mock_cred + self.raw_value = mock_cred + + def decode(self): + return MOCK_CRED + + +class MockCredScan: + value = [MockCredEntry()] + name = "name" + + def __aiter__(self): + return self + + async def __anext__(self): + if self.value: + return self.value.pop() + raise StopAsyncIteration + + +class MockMimeTypeRecord: + value_json = {"mime-type": "mime-type"} + + +@pytest.mark.anoncreds +class TestAnonCredsHolder(TestCase): + def setUp(self) -> None: + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar-anoncreds"}, + profile_class=AskarAnoncredsProfile, + ) + self.holder = test_module.AnonCredsHolder(self.profile) + + async def test_init(self): + assert isinstance(self.holder, AnonCredsHolder) + assert isinstance(self.holder.profile, AskarAnoncredsProfile) + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_get_master_secret(self, mock_session_handle): + mock_session_handle.fetch = mock.CoroutineMock(return_value=MockMasterSecret()) + secret = await self.holder.get_master_secret() + assert secret is not None + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_get_master_secret_errors(self, mock_session_handle): + # Not found + mock_session_handle.fetch = mock.CoroutineMock( + side_effect=[AskarError(code=AskarErrorCode.NOT_FOUND, message="test")] + ) + with self.assertRaises(AnonCredsHolderError): + await self.holder.get_master_secret() + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_get_master_secret_does_not_return_master_secret( + self, mock_session_handle + ): + # Duplicate - Retry + mock_session_handle.fetch = mock.CoroutineMock(return_value=None) + mock_session_handle.insert = mock.CoroutineMock( + side_effect=[AskarError(code=AskarErrorCode.DUPLICATE, message="test")] + ) + with self.assertRaises(StopAsyncIteration): + await self.holder.get_master_secret() + # Other error + mock_session_handle.fetch = mock.CoroutineMock(return_value=None) + mock_session_handle.insert = mock.CoroutineMock( + side_effect=[AskarError(code=AskarErrorCode.UNEXPECTED, message="test")] + ) + with self.assertRaises(AnonCredsHolderError): + await self.holder.get_master_secret() + + @mock.patch.object( + AnonCredsHolder, "get_master_secret", return_value="master-secret" + ) + @mock.patch.object( + CredentialRequest, + "create", + return_value=(mock.Mock(), mock.Mock()), + ) + async def test_create_credential_request( + self, mock_credential_request, mock_master_secret + ): + cred_def = CredDef( + issuer_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + schema_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ/anoncreds/v0/SCHEMA/MemberPass/1.0", + tag="tag", + type="CL", + value=CredDefValue(primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z")), + ) + # error - to_native_fails + with self.assertRaises(AnonCredsHolderError): + await self.holder.create_credential_request( + {"offer": "offer"}, + cred_def, + "holder-did", + ) + + # Need to mock or else it will get ssl error + cred_def.to_native = mock.MagicMock(return_value="native-cred-request") + + ( + cred_req_json, + cred_req_metadata_json, + ) = await self.holder.create_credential_request( + {"offer": "offer"}, + cred_def, + "holder-did", + ) + + assert cred_req_json is not None + assert cred_req_metadata_json is not None + assert mock_master_secret.called + assert mock_credential_request.called + + async def test_create_credential_request_with_non_anoncreds_profile_throws_x(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar"}, + profile_class=IndySdkProfile, + ) + self.holder = test_module.AnonCredsHolder(self.profile) + with self.assertRaises(ValueError): + await self.holder.create_credential_request( + {"offer": "offer"}, + CredDef( + issuer_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + schema_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ/anoncreds/v0/SCHEMA/MemberPass/1.0", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z") + ), + ), + "holder-did", + ) + + @mock.patch.object( + AnonCredsHolder, "get_master_secret", return_value="master-secret" + ) + async def test_store_credential_fails_to_load_raises_x(self, mock_master_secret): + with self.assertRaises(AnonCredsHolderError): + await self.holder.store_credential( + {"cred-def": "cred-def"}, + { + "values": [ + "name", + "date", + "degree", + "birthdate_dateint", + "timestamp", + ] + }, + {"cred-req-meta": "cred-req-meta"}, + ) + assert mock_master_secret.called + + @mock.patch.object( + AnonCredsHolder, "get_master_secret", return_value="master-secret" + ) + @mock.patch.object( + Credential, + "load", + side_effect=[ + MockCredential(), + MockCredential(), + MockCredential(bad_schema=True), + MockCredential(bad_cred_def=True), + ], + ) + async def test_store_credential(self, mock_load, mock_master_secret): + self.profile.transaction = mock.Mock( + return_value=mock.MagicMock( + insert=mock.CoroutineMock(return_value=None), + commit=mock.CoroutineMock(return_value=None), + ) + ) + + # Valid + result = await self.holder.store_credential( + MOCK_CRED_DEF, + MOCK_CRED, + {"cred-req-meta": "cred-req-meta"}, + credential_attr_mime_types={"first_name": "application/json"}, + ) + + assert result is not None + assert mock_master_secret.called + assert mock_load.called + assert self.profile.transaction.called + + # Testing normalizing attr names + await self.holder.store_credential( + MOCK_CRED_DEF, + { + "values": { + "First Name": {"raw": "Alice", "encoded": "113...335"}, + }, + }, + {"cred-req-meta": "cred-req-meta"}, + ) + + # Test bad id's + with self.assertRaises(AnonCredsHolderError): + await self.holder.store_credential( + MOCK_CRED_DEF, + MOCK_PRES, + {"cred-req-meta": "cred-req-meta"}, + ) + with self.assertRaises(AnonCredsHolderError): + await self.holder.store_credential( + MOCK_CRED_DEF, + MOCK_CRED, + {"cred-req-meta": "cred-req-meta"}, + ) + + @mock.patch.object( + AnonCredsHolder, "get_master_secret", return_value="master-secret" + ) + @mock.patch.object(Credential, "load", return_value=MockCredential()) + async def test_store_credential_failed_trx(self, mock_load, mock_master_secret): + self.profile.transaction = mock.MagicMock( + side_effect=[AskarError(AskarErrorCode.UNEXPECTED, "test")] + ) + + with self.assertRaises(AnonCredsHolderError): + await self.holder.store_credential( + MOCK_CRED_DEF, + MOCK_CRED, + {"cred-req-meta": "cred-req-meta"}, + credential_attr_mime_types={"first_name": "application/json"}, + ) + + async def test_get_credentials(self): + self.profile.store = mock.Mock() + self.profile.store.scan = mock.Mock(return_value=MockCredScan()) + + result = await self.holder.get_credentials(0, 10, {}) + assert isinstance(result, list) + assert len(result) == 1 + + async def test_get_credentials_errors(self): + self.profile.store = mock.Mock() + self.profile.store.scan = mock.Mock( + side_effect=[ + AskarError(AskarErrorCode.UNEXPECTED, "test"), + AnoncredsError(AnoncredsErrorCode.UNEXPECTED, "test"), + ] + ) + + with self.assertRaises(AnonCredsHolderError): + await self.holder.get_credentials(0, 10, {}) + with self.assertRaises(AnonCredsHolderError): + await self.holder.get_credentials(0, 10, {}) + + async def test_get_credentials_for_presentation_request_by_referent(self): + self.profile.store = mock.Mock() + self.profile.store.scan = mock.Mock( + return_value=mock.CoroutineMock( + side_effect=[ + MockCredScan(), + ] + ) + ) + + # add predicates + mock_pres_req = deepcopy(MOCK_PRES_REQ) + mock_pres_req["requested_predicates"]["0_concentration_GE_uuid"] = { + "name": "concentration", + "p_type": "<=", + "p_value": 9, + "restrictions": [{"schema_name": "MYCO Biomarker"}], + } + await self.holder.get_credentials_for_presentation_request_by_referent( + mock_pres_req, None, start=0, count=10 + ) + + # non-existent referent + with self.assertRaises(AnonCredsHolderError): + await self.holder.get_credentials_for_presentation_request_by_referent( + mock_pres_req, "not-found-ref", start=0, count=10 + ) + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_get_credential(self, mock_handle): + mock_handle.fetch = mock.CoroutineMock(side_effect=[MockCredEntry(), None]) + result = await self.holder.get_credential("cred-id") + assert isinstance(result, str) + + with self.assertRaises(WalletNotFoundError): + await self.holder.get_credential("cred-id") + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_credential_revoked(self, mock_handle): + mock_ledger = mock.MagicMock( + get_revoc_reg_delta=mock.CoroutineMock( + return_value=( + { + "value": { + "revoked": [100], + } + }, + 0, + ), + ) + ) + mock_handle.fetch = mock.CoroutineMock(return_value=MockCredEntry()) + assert ( + await self.holder.credential_revoked( + ledger=mock_ledger, + credential_id="cred-id", + to=None, + fro=None, + ) + is False + ) + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_delete_credential(self, mock_handle): + mock_handle.remove = mock.CoroutineMock( + side_effect=[ + None, + None, + AskarError(AskarErrorCode.NOT_FOUND, "test"), + AskarError(AskarErrorCode.UNEXPECTED, "test"), + ] + ) + await self.holder.delete_credential("cred-id") + + mock_handle.remove.call_args_list[0].args == ("credential", "cred-id") + mock_handle.remove.call_args_list[0].args == ("attribute-mime-types", "cred-id") + + # not found, don't raise error + await self.holder.delete_credential("cred-id") + # other asker error, raise error + with self.assertRaises(AnonCredsHolderError): + await self.holder.delete_credential("cred-id") + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_get_mime_type(self, mock_handle): + mock_handle.fetch = mock.CoroutineMock( + side_effect=[ + MockMimeTypeRecord(), + AskarError(AskarErrorCode.UNEXPECTED, "test"), + ] + ) + result = await self.holder.get_mime_type("cred-id", "mime-type") + assert result == "mime-type" + + # asker error + with self.assertRaises(AnonCredsHolderError): + await self.holder.get_mime_type("cred-id", "mime-type") + assert mock_handle.fetch.call_count == 2 + + @mock.patch.object(InMemoryProfileSession, "handle") + @mock.patch.object( + AnonCredsHolder, "get_master_secret", return_value="master-secret" + ) + @mock.patch.object( + anoncreds.Presentation, "create", return_value=Presentation.load(MOCK_PRES) + ) + async def test_create_presentation( + self, mock_pres_create, mock_master_secret, mock_handle + ): + mock_handle.fetch = mock.CoroutineMock(return_value=MockCredEntry()) + result = await self.holder.create_presentation( + presentation_request=MOCK_PRES_REQ, + requested_credentials={ + "self_attested_attributes": {}, + "requested_attributes": {}, + "requested_predicates": {}, + }, + schemas={}, + credential_definitions={}, + rev_states={}, + ) + + json.loads(result) + assert mock_pres_create.called + assert mock_master_secret.called + mock_handle.fetch.assert_called + + # requested_attributes and predicates + await self.holder.create_presentation( + presentation_request=MOCK_PRES_REQ, + requested_credentials={ + "self_attested_attributes": {}, + "requested_attributes": { + "biomarker_attrs_0": { + "cred_id": "cred-id-requested", + "revealed": True, + }, + }, + "requested_predicates": { + "0_concentration_GE_uuid": { + "cred_id": "cred-id-predicate", + } + }, + }, + schemas={}, + credential_definitions={}, + rev_states={}, + ) + + @mock.patch.object(InMemoryProfileSession, "handle") + @mock.patch.object( + AnonCredsHolder, "get_master_secret", return_value="master-secret" + ) + @mock.patch.object( + anoncreds.Presentation, "create", return_value=Presentation.load(MOCK_PRES) + ) + @mock.patch.object(PresentCredentials, "add_attributes") + async def test_create_presentation_with_revocation( + self, mock_add_attributes, mock_pres_create, mock_master_secret, mock_handle + ): + mock_handle.fetch = mock.CoroutineMock(return_value=MockCredEntry(rev_reg=True)) + + # not in rev_states + with self.assertRaises(AnonCredsHolderError): + await self.holder.create_presentation( + presentation_request=MOCK_PRES_REQ, + requested_credentials={ + "self_attested_attributes": {}, + "requested_attributes": { + "biomarker_attrs_0": { + "cred_id": "cred-id-requested", + "revealed": True, + "timestamp": 1234567890, + }, + }, + "requested_predicates": { + "0_concentration_GE_uuid": { + "cred_id": "cred-id-predicate", + } + }, + }, + schemas={}, + credential_definitions={}, + rev_states={}, + ) + # wrong timestamp + with self.assertRaises(AnonCredsHolderError): + await self.holder.create_presentation( + presentation_request=MOCK_PRES_REQ, + requested_credentials={ + "self_attested_attributes": {}, + "requested_attributes": { + "biomarker_attrs_0": { + "cred_id": "cred-id-requested", + "revealed": True, + "timestamp": 1234567890, + }, + }, + "requested_predicates": { + "0_concentration_GE_uuid": { + "cred_id": "cred-id-predicate", + } + }, + }, + schemas={}, + credential_definitions={}, + rev_states={ + "rev-reg-id": { + "9999999999": b"100", + } + }, + ) + + await self.holder.create_presentation( + presentation_request=MOCK_PRES_REQ, + requested_credentials={ + "self_attested_attributes": {}, + "requested_attributes": { + "biomarker_attrs_0": { + "cred_id": "cred-id-requested", + "revealed": True, + "timestamp": 1234567890, + }, + }, + "requested_predicates": { + "0_concentration_GE_uuid": { + "cred_id": "cred-id-predicate", + } + }, + }, + schemas={}, + credential_definitions={}, + rev_states={ + "rev-reg-id": { + 1234567890: b'{"witness":{"omega": "21 124...AC8"}}', + } + }, + ) + + assert mock_add_attributes.called + assert mock_pres_create.called + assert mock_master_secret.called + assert mock_handle.fetch.called + + @mock.patch.object(InMemoryProfileSession, "handle") + @mock.patch.object( + AnonCredsHolder, "get_master_secret", return_value="master-secret" + ) + @mock.patch.object( + anoncreds.Presentation, + "create", + side_effect=AnoncredsError(AnoncredsErrorCode.UNEXPECTED, "test"), + ) + async def test_create_presentation_create_error( + self, mock_pres_create, mock_master_secret, mock_handle + ): + mock_handle.fetch = mock.CoroutineMock(return_value=MockCredEntry()) + # anoncreds error when creating presentation + with self.assertRaises(AnonCredsHolderError): + await self.holder.create_presentation( + presentation_request=MOCK_PRES_REQ, + requested_credentials={ + "self_attested_attributes": {}, + "requested_attributes": {}, + "requested_predicates": {}, + }, + schemas={}, + credential_definitions={}, + rev_states={}, + ) + + @mock.patch.object( + CredentialRevocationState, + "create", + ) + async def test_create_revocation_state(self, mock_create): + schema = Schema.create( + name="MemberPass", + attr_names=["member", "score"], + issuer_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + version="1.0", + ) + + (cred_def, _, _) = CredentialDefinition.create( + schema_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ/anoncreds/v0/SCHEMA/MemberPass/1.0", + schema=schema, + issuer_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + tag="tag", + support_revocation=True, + signature_type="CL", + ) + (rev, _) = RevocationRegistryDefinition.create( + cred_def_id="SGrjRL82Y9ZZbzhUDXokvQ:3:CL:531757:MemberPass", + cred_def=cred_def, + issuer_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + registry_type="CL_ACCUM", + max_cred_num=100, + tag="tag", + ) + mock_create.return_value = rev + result = await self.holder.create_revocation_state( + cred_rev_id="1", + rev_reg_def={"def": 1}, + rev_list={"accum": "1"}, + tails_file_path="/tmp/some.tails", + ) + + result = json.loads(result) + assert mock_create.called + + # error + mock_create.side_effect = AnoncredsError(AnoncredsErrorCode.UNEXPECTED, "test") + with self.assertRaises(AnonCredsHolderError): + await self.holder.create_revocation_state( + cred_rev_id="1", + rev_reg_def={"def": 1}, + rev_list={"accum": "1"}, + tails_file_path="/tmp/some.tails", + ) diff --git a/aries_cloudagent/anoncreds/tests/test_issuer.py b/aries_cloudagent/anoncreds/tests/test_issuer.py index b569fb4d72..00a963f1cd 100644 --- a/aries_cloudagent/anoncreds/tests/test_issuer.py +++ b/aries_cloudagent/anoncreds/tests/test_issuer.py @@ -1,8 +1,727 @@ -""" -Unit tests for the anoncreds issuer: +import json +from typing import Optional -- create schema and cred def -- create offer and issue credential +import pytest +from anoncreds import ( + Credential, + CredentialDefinition, + CredentialOffer, +) +from aries_askar import AskarError, AskarErrorCode +from asynctest import TestCase -See tests under aries_cloudagent/indy/sdk/tests -""" +from aries_cloudagent.anoncreds.base import ( + AnonCredsObjectAlreadyExists, + AnonCredsSchemaAlreadyExists, +) +from aries_cloudagent.anoncreds.models.anoncreds_cred_def import ( + CredDef, + CredDefResult, + CredDefState, + CredDefValue, + CredDefValuePrimary, + CredDefValueRevocation, + GetCredDefResult, +) +from aries_cloudagent.anoncreds.models.anoncreds_schema import ( + AnonCredsSchema, + GetSchemaResult, + SchemaResult, + SchemaState, +) +from aries_cloudagent.askar.profile_anon import ( + AskarAnoncredsProfile, +) +from aries_cloudagent.core.event_bus import Event, MockEventBus +from aries_cloudagent.core.in_memory.profile import ( + InMemoryProfile, + InMemoryProfileSession, +) +from aries_cloudagent.indy.sdk.profile import IndySdkProfile +from aries_cloudagent.tests import mock + +from .. import issuer as test_module + + +class MockSchemaEntry: + def __init__(self, name: Optional[str] = "name"): + self.name = name + self.version = "1.0" + self.value_json = "value_json" + self.issuer_id = "issuer-id" + + def serialize(self): + return self.value_json + + +class MockSchemaValue: + attr_names = ["attr1", "attr2"] + + +class MockSchemaResult: + def __init__(self) -> None: + self.schema = MockSchemaEntry() + self.schema_value = MockSchemaValue() + + +class MockCredDefState: + credential_definition_id = "cred-def-id" + state = "finished" + + +class MockCredDefEntry: + def __init__(self, name: Optional[str] = "name", epoch: Optional[str] = None): + self.name = name + self.tags = { + "schema_id": "schema-id", + "epoch": epoch, + } + + credential_definition_state = MockCredDefState() + raw_value = "raw-value" + + def to_json(self): + return json.dumps({"cred_def": "cred_def"}) + + +class MockCredDefPrivate: + def to_json_buffer(self): + return "cred-def-private" + + +class MockKeyProof: + def to_json_buffer(self): + return "key-proof" + + raw_value = "raw-value" + + +class MockCredOffer: + def to_json(self): + return json.dumps({"cred_offer": "cred_offer"}) + + +class MockCredential: + def to_json(self): + return json.dumps({"credential": "credential"}) + + +def get_mock_schema_result( + job_id: Optional[str] = "job-id", schema_id: Optional[str] = "schema-id" +): + return SchemaResult( + job_id=job_id, + schema_state=SchemaState( + state="finished", + schema_id=schema_id, + schema=AnonCredsSchema( + issuer_id="issuer-id", + name="name", + version="1.0", + attr_names=["attr1", "attr2"], + ), + ), + ) + + +@pytest.mark.anoncreds +class TestAnonCredsIssuer(TestCase): + def setUp(self) -> None: + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar-anoncreds"}, + profile_class=AskarAnoncredsProfile, + ) + self.issuer = test_module.AnonCredsIssuer(self.profile) + + async def test_init(self): + assert isinstance(self.issuer, test_module.AnonCredsIssuer) + assert isinstance(self.issuer.profile, AskarAnoncredsProfile) + + async def test_init_wrong_profile_type(self): + self.issuer._profile = InMemoryProfile.test_profile( + profile_class=IndySdkProfile + ) + with self.assertRaises(ValueError): + self.issuer.profile + + async def test_notify(self): + self.profile.inject = mock.Mock(return_value=MockEventBus()) + await self.issuer.notify(Event(topic="test-topic")) + + @mock.patch.object(InMemoryProfileSession, "handle") + @mock.patch.object(AnonCredsSchema, "deserialize", return_value="test") + async def test_create_and_register_schema_finds_schema_raises_x( + self, _, mock_session_handle + ): + mock_schema = AnonCredsSchema( + issuer_id="issuer-id", + name="schema-name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + mock_schema.value_json = "value_json" + mock_session_handle.fetch_all = mock.CoroutineMock(return_value=[mock_schema]) + with self.assertRaises(AnonCredsObjectAlreadyExists): + await self.issuer.create_and_register_schema( + issuer_id="issuer-id", + name="name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_create_and_register_schema(self, mock_session_handle): + mock_session_handle.fetch_all = mock.CoroutineMock(return_value=[]) + mock_session_handle.insert = mock.CoroutineMock(return_value=None) + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + register_schema=mock.CoroutineMock( + return_value=get_mock_schema_result() + ) + ) + ) + result = await self.issuer.create_and_register_schema( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + name="example name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + + assert result is not None + mock_session_handle.fetch_all.assert_called_once() + mock_session_handle.insert.assert_called_once() + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_create_and_register_schema_missing_schema_id_or_job_id( + self, mock_session_handle + ): + mock_session_handle.fetch_all = mock.CoroutineMock(return_value=[]) + mock_session_handle.insert = mock.CoroutineMock(return_value=None) + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + register_schema=mock.CoroutineMock( + side_effect=[ + SchemaResult( + job_id=None, + schema_state=SchemaState( + state="finished", + schema_id=None, + schema=AnonCredsSchema( + issuer_id="issuer-id", + name="name", + version="1.0", + attr_names=["attr1", "attr2"], + ), + ), + ), + SchemaResult( + job_id=None, + schema_state=SchemaState( + state="finished", + schema_id="schema-id", + schema=AnonCredsSchema( + issuer_id="issuer-id", + name="name", + version="1.0", + attr_names=["attr1", "attr2"], + ), + ), + ), + SchemaResult( + job_id="job-id", + schema_state=SchemaState( + state="finished", + schema_id=None, + schema=AnonCredsSchema( + issuer_id="issuer-id", + name="name", + version="1.0", + attr_names=["attr1", "attr2"], + ), + ), + ), + ] + ) + ) + ) + + with self.assertRaises(ValueError): + await self.issuer.create_and_register_schema( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + name="example name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + + mock_session_handle.fetch_all.assert_called_once() + mock_session_handle.insert.assert_not_called + + await self.issuer.create_and_register_schema( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + name="example name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + await self.issuer.create_and_register_schema( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + name="example name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_create_and_register_schema_fail_insert(self, mock_session_handle): + mock_session_handle.fetch_all = mock.CoroutineMock(return_value=[]) + mock_session_handle.insert = mock.CoroutineMock( + side_effect=AskarError(AskarErrorCode.UNEXPECTED, message="test-msg") + ) + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + register_schema=mock.CoroutineMock( + return_value=get_mock_schema_result() + ) + ) + ) + + with self.assertRaises(test_module.AnonCredsIssuerError): + result = await self.issuer.create_and_register_schema( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + name="example name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + + assert result is not None + mock_session_handle.fetch_all.assert_called_once() + mock_session_handle.insert.assert_called_once() + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_create_and_register_already_exists_but_not_in_wallet( + self, mock_session_handle + ): + mock_session_handle.fetch_all = mock.CoroutineMock(return_value=[]) + mock_session_handle.insert = mock.CoroutineMock(return_value=None) + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + register_schema=mock.CoroutineMock( + side_effect=AnonCredsSchemaAlreadyExists( + message="message", + obj_id="id", + obj=AnonCredsSchema( + issuer_id="issuer-id", + name="schema-name", + version="1.0", + attr_names=["attr1", "attr2"], + ), + ) + ) + ) + ) + with self.assertRaises(test_module.AnonCredsIssuerError): + await self.issuer.create_and_register_schema( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + name="example", + version="1.0", + attr_names=["attr1", "attr2"], + ) + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_create_and_register_schema_without_job_id_or_schema_id_raises_x( + self, mock_session_handle + ): + mock_session_handle.fetch_all = mock.CoroutineMock(return_value=[]) + mock_session_handle.insert = mock.CoroutineMock(return_value=None) + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + register_schema=mock.CoroutineMock( + side_effect=[ + get_mock_schema_result(job_id=None, schema_id=None), + get_mock_schema_result(job_id=None), + get_mock_schema_result(schema_id=None), + ] + ) + ) + ) + with self.assertRaises(ValueError): + await self.issuer.create_and_register_schema( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + name="name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + await self.issuer.create_and_register_schema( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + name="name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + await self.issuer.create_and_register_schema( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + name="name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + + async def test_finish_schema(self): + self.profile.transaction = mock.Mock( + return_value=mock.MagicMock( + commit=mock.CoroutineMock(return_value=None), + ) + ) + await self.issuer.finish_schema(job_id="job-id", schema_id="schema-id") + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_get_created_schemas(self, mock_session_handle): + mock_session_handle.fetch_all = mock.CoroutineMock( + return_value=[MockSchemaEntry("name-test")] + ) + result = await self.issuer.get_created_schemas() + mock_session_handle.fetch_all.assert_called_once() + assert result == ["name-test"] + + mock_session_handle.fetch_all = mock.CoroutineMock( + return_value=[ + MockSchemaEntry("schema1"), + MockSchemaEntry("schema2"), + ] + ) + result = await self.issuer.get_created_schemas() + mock_session_handle.fetch_all.assert_called_once() + assert result == ["schema1", "schema2"] + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_credential_definition_in_wallet(self, mock_session_handle): + mock_session_handle.fetch = mock.CoroutineMock( + side_effect=[ + CredDef( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z") + ), + ), + None, + AskarError(AskarErrorCode.UNEXPECTED, message="test-msg"), + ] + ) + assert await self.issuer.credential_definition_in_wallet("cred-def-id") is True + assert await self.issuer.credential_definition_in_wallet("cred-def-id") is False + with self.assertRaises(test_module.AnonCredsIssuerError): + await self.issuer.credential_definition_in_wallet("cred-def-id") + + async def test_create_and_register_credential_definition_invalid_options_raises_x( + self, + ): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_schema=mock.CoroutineMock( + return_value=AnonCredsSchema( + issuer_id="issuer-id", + name="schema-name", + version="1.0", + attr_names=["attr1", "attr2"], + ) + ) + ) + ) + with self.assertRaises(ValueError): + await self.issuer.create_and_register_credential_definition( + issuer_id="issuer-id", + schema_id="schema-id", + signature_type="CL", + options={"support_revocation": "true"}, # requires boolean + ) + with self.assertRaises(ValueError): + await self.issuer.create_and_register_credential_definition( + issuer_id="issuer-id", + schema_id="schema-id", + signature_type="CL", + options={"max_cred_num": "100"}, # requires integer + ) + + @mock.patch.object(test_module.AnonCredsIssuer, "notify") + async def test_create_and_register_credential_definition_finishes( + self, mock_notify + ): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_schema=mock.CoroutineMock( + return_value=GetSchemaResult( + schema_id="schema-id", + schema=AnonCredsSchema( + issuer_id="issuer-id", + name="schema-name", + version="1.0", + attr_names=["attr1", "attr2"], + ), + schema_metadata={}, + resolution_metadata={}, + ) + ), + register_credential_definition=mock.CoroutineMock( + return_value=CredDefResult( + job_id="job-id", + credential_definition_state=CredDefState( + state="finished", + credential_definition=CredDef( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary( + "n", "s", {}, "rctxt", "z" + ) + ), + ), + credential_definition_id="cred-def-id", + ), + credential_definition_metadata={}, + registration_metadata={}, + ) + ), + ) + ) + self.profile.transaction = mock.Mock( + return_value=mock.MagicMock( + insert=mock.CoroutineMock(return_value=None), + commit=mock.CoroutineMock(return_value=None), + ) + ) + result = await self.issuer.create_and_register_credential_definition( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + schema_id="CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3", + signature_type="CL", + options={}, + ) + + assert isinstance(result, CredDefResult) + mock_notify.assert_called_once() + + @mock.patch.object(test_module.AnonCredsIssuer, "notify") + async def test_create_and_register_credential_definition_errors(self, mock_notify): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_schema=mock.CoroutineMock( + return_value=GetSchemaResult( + schema_id="schema-id", + schema=AnonCredsSchema( + issuer_id="issuer-id", + name="schema-name", + version="1.0", + attr_names=["attr1", "attr2"], + ), + schema_metadata={}, + resolution_metadata={}, + ) + ), + # No job_id or cred_def_id + register_credential_definition=mock.CoroutineMock( + side_effect=[ + CredDefResult( + job_id=None, + credential_definition_state=CredDefState( + state="finished", + credential_definition=CredDef( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary( + "n", "s", {}, "rctxt", "z" + ) + ), + ), + credential_definition_id=None, + ), + credential_definition_metadata={}, + registration_metadata={}, + ), + ] + ), + ) + ) + self.profile.transaction = mock.Mock( + return_value=mock.MagicMock( + insert=mock.CoroutineMock(return_value=None), + commit=mock.CoroutineMock(return_value=None), + ) + ) + # Creating fails + with self.assertRaises(test_module.AnonCredsIssuerError): + await self.issuer.create_and_register_credential_definition( + issuer_id="issuer-id", + schema_id="CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3", + signature_type="CL", + options={}, + ) + # No job_id or cred_def_id + with self.assertRaises(test_module.AnonCredsIssuerError): + await self.issuer.create_and_register_credential_definition( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + schema_id="CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3", + signature_type="CL", + options={}, + ) + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_get_created_cred_defs(self, mock_session_handle): + mock_session_handle.fetch_all = mock.CoroutineMock( + return_value=[MockCredDefEntry()] + ) + result = await self.issuer.get_created_credential_definitions() + mock_session_handle.fetch_all.assert_called_once() + assert result == ["name"] + mock_session_handle.fetch_all = mock.CoroutineMock( + return_value=[MockCredDefEntry("cred_def1"), MockCredDefEntry("cred_def2")] + ) + result = await self.issuer.get_created_credential_definitions( + issuer_id="issuer-id-test", + schema_issuer_id="schema-issuer-id-test", + schema_id="schema-id-test", + schema_name="schema-name-test", + schema_version="schema-version-test", + epoch="123456789", + ) + mock_session_handle.fetch_all.assert_called_once() + assert result == ["cred_def1", "cred_def2"] + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_match_created_cred_defs(self, mock_session_handle): + mock_session_handle.fetch_all = mock.CoroutineMock( + return_value=[ + MockCredDefEntry(name="name2", epoch="2"), + MockCredDefEntry(name="name3", epoch="3"), + MockCredDefEntry(name="name4", epoch="4"), + MockCredDefEntry(name="name1", epoch="1"), + ] + ) + result = await self.issuer.match_created_credential_definitions() + assert result == "name4" + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_create_credential_offer_cred_def_not_found( + self, mock_session_handle + ): + # None, Valid + # Valid, None + # None, None + mock_session_handle.fetch = mock.CoroutineMock( + side_effect=[None, MockKeyProof(), MockCredDefEntry(), None, None, None] + ) + with self.assertRaises(test_module.AnonCredsIssuerError): + await self.issuer.create_credential_offer("cred-def-id") + with self.assertRaises(test_module.AnonCredsIssuerError): + await self.issuer.create_credential_offer("cred-def-id") + with self.assertRaises(test_module.AnonCredsIssuerError): + await self.issuer.create_credential_offer("cred-def-id") + + async def test_cred_def_supports_revocation(self): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential_definition=mock.CoroutineMock( + side_effect=[ + GetCredDefResult( + credential_definition_id="cred-def-id", + credential_definition=CredDef( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary( + "n", "s", {}, "rctxt", "z" + ) + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ), + GetCredDefResult( + credential_definition_id="cred-def-id", + credential_definition=CredDef( + issuer_id="did:sov:3avoBCqDMFHFaKUHug9s8W", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary( + "n", "s", {}, "rctxt", "z" + ), + revocation=CredDefValueRevocation( + g="g", + g_dash="g_dash", + h="h", + h0="h0", + h1="h1", + h2="h2", + h_cap="h_cap", + htilde="htilde", + pk="pk", + u="u", + y="y", + ), + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ), + ] + ) + ) + ) + + result = await self.issuer.cred_def_supports_revocation("cred-def-id") + assert result is False + result = await self.issuer.cred_def_supports_revocation("cred-def-id") + assert result is True + + @mock.patch.object(InMemoryProfileSession, "handle") + @mock.patch.object(CredentialDefinition, "load", return_value=MockCredDefEntry()) + async def test_create_credential_offer_create_fail( + self, mock_load, mock_session_handle + ): + mock_session_handle.fetch = mock.CoroutineMock( + side_effect=[MockCredDefEntry(), MockKeyProof()] + ) + with self.assertRaises(test_module.AnonCredsIssuerError): + await self.issuer.create_credential_offer("cred-def-id") + assert mock_session_handle.fetch.called + assert mock_load.called + + @mock.patch.object(InMemoryProfileSession, "handle") + @mock.patch.object(CredentialDefinition, "load", return_value=MockCredDefEntry()) + @mock.patch.object(CredentialOffer, "create", return_value=MockCredOffer()) + async def test_create_credential_offer_create( + self, mock_create, mock_load, mock_session_handle + ): + mock_session_handle.fetch = mock.CoroutineMock( + side_effect=[MockCredDefEntry(), MockKeyProof()] + ) + result = await self.issuer.create_credential_offer("cred-def-id") + assert mock_session_handle.fetch.called + assert mock_load.called + assert mock_create.called + assert result is not None + + @mock.patch.object(InMemoryProfileSession, "handle") + @mock.patch.object(Credential, "create", return_value=MockCredential()) + async def test_create_credential(self, mock_create, mock_session_handle): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_schema=mock.CoroutineMock(return_value=MockSchemaResult()) + ) + ) + mock_session_handle.fetch = mock.CoroutineMock(return_value=MockCredDefEntry()) + result = await self.issuer.create_credential( + {"schema_id": "schema-id", "cred_def_id": "cred-def-id"}, + {}, + {"attr1": "value1", "attr2": "value2"}, + ) + + assert result is not None + assert mock_session_handle.fetch.called + assert mock_create.called diff --git a/aries_cloudagent/anoncreds/tests/test_routes.py b/aries_cloudagent/anoncreds/tests/test_routes.py index fab2633360..e508daf6e7 100644 --- a/aries_cloudagent/anoncreds/tests/test_routes.py +++ b/aries_cloudagent/anoncreds/tests/test_routes.py @@ -1,34 +1,347 @@ -from asynctest import mock as async_mock, TestCase as AsyncTestCase +import json + +import pytest +from aiohttp import web +from asynctest import TestCase as AsyncTestCase + +from aries_cloudagent.admin.request_context import AdminRequestContext +from aries_cloudagent.anoncreds.issuer import AnonCredsIssuer +from aries_cloudagent.anoncreds.revocation import AnonCredsRevocation +from aries_cloudagent.anoncreds.revocation_setup import DefaultRevocationSetup +from aries_cloudagent.askar.profile_anon import AskarAnoncredsProfile +from aries_cloudagent.core.in_memory.profile import InMemoryProfile +from aries_cloudagent.revocation_anoncreds.manager import RevocationManager +from aries_cloudagent.tests import mock + from .. import routes as test_module +class MockSchema: + def __init__(self, schema_id): + self.schemaId = schema_id + + def serialize(self): + return {"schema_id": self.schemaId} + + +class MockCredentialDefinition: + def __init__(self, cred_def_id): + self.credDefId = cred_def_id + + def serialize(self): + return {"credential_definition_id": self.credDefId} + + +class MockRovocationRegistryDefinition: + def __init__(self, rev_reg_id): + self.revRegId = rev_reg_id + + def serialize(self): + return {"revocation_registry_definition_id": self.revRegId} + + +@pytest.mark.anoncreds class TestAnoncredsRoutes(AsyncTestCase): + async def setUp(self) -> None: + self.session_inject = {} + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar-anoncreds"}, + profile_class=AskarAnoncredsProfile, + ) + self.context = AdminRequestContext.test_context(self.session_inject) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + + @mock.patch.object( + AnonCredsIssuer, + "create_and_register_schema", + return_value=MockSchema("schemaId"), + ) + async def test_schemas_post(self, mock_create_and_register_schema): + self.request.json = mock.CoroutineMock( + side_effect=[ + { + "schema": { + "issuerId": "Q4TmbeGPoWeWob4Xf6KetA", + "attrNames": ["score"], + "name": "Example Schema", + "version": "0.0.1", + } + }, + {}, + {"schema": {}}, + ] + ) + result = await test_module.schemas_post(self.request) + assert json.loads(result.body)["schema_id"] == "schemaId" + + assert mock_create_and_register_schema.call_count == 1 + + # TODO: consider handling missing schema attribute better + with self.assertRaises(AttributeError): + await test_module.schemas_post(self.request) + + await test_module.schemas_post(self.request) + + async def test_get_schema(self): + self.request.match_info = {"schemaId": "schema_id"} + self.context.inject = mock.Mock( + return_value=mock.MagicMock( + get_schema=mock.CoroutineMock(return_value=MockSchema("schemaId")) + ) + ) + result = await test_module.schema_get(self.request) + assert json.loads(result.body)["schema_id"] == "schemaId" + + self.request.match_info = {} + with self.assertRaises(KeyError): + await test_module.schema_get(self.request) + + @mock.patch.object( + AnonCredsIssuer, + "get_created_schemas", + side_effect=[ + [ + "Q4TmbeGPoWeWob4Xf6KetA:2:Example Schema:0.0.1", + "Q4TmbeGPoWeWob4Xf6KetA:2:Example Schema:0.0.2", + ], + [], + ], + ) + async def test_get_schemas(self, mock_get_created_schemas): + result = await test_module.schemas_get(self.request) + assert json.loads(result.body)["schema_ids"].__len__() == 2 + + result = await test_module.schemas_get(self.request) + assert json.loads(result.body)["schema_ids"].__len__() == 0 + + assert mock_get_created_schemas.call_count == 2 + + @mock.patch.object( + AnonCredsIssuer, + "create_and_register_credential_definition", + return_value=MockCredentialDefinition("credDefId"), + ) + async def test_cred_def_post(self, mock_create_cred_def): + self.request.json = mock.CoroutineMock( + side_effect=[ + { + "credential_definition": { + "issuerId": "issuerId", + "schemaId": "schemaId", + "tag": "tag", + }, + "options": { + "endorser_connection_id": "string", + "revocation_registry_size": 0, + "support_revocation": True, + }, + }, + {}, + {"credential_definition": {}}, + ] + ) + + result = await test_module.cred_def_post(self.request) + + assert json.loads(result.body)["credential_definition_id"] == "credDefId" + assert mock_create_cred_def.call_count == 1 + + # TODO: consider handling missing cred_def attribute better + with self.assertRaises(AttributeError): + await test_module.cred_def_post(self.request) + + await test_module.cred_def_post(self.request) + + async def test_cred_def_get(self): + self.request.match_info = {"cred_def_id": "cred_def_id"} + self.context.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential_definition=mock.CoroutineMock( + return_value=MockCredentialDefinition("credDefId") + ) + ) + ) + result = await test_module.cred_def_get(self.request) + assert json.loads(result.body)["credential_definition_id"] == "credDefId" + + self.request.match_info = {} + with self.assertRaises(KeyError): + await test_module.cred_def_get(self.request) + + @mock.patch.object( + AnonCredsIssuer, + "get_created_credential_definitions", + side_effect=[ + [ + "Q4TmbeGPoWeWob4Xf6KetA:3:CL:229927:tag", + "Q4TmbeGPoWeWob4Xf6KetA:3:CL:229925:faber.agent.degree_schema", + ], + [], + ], + ) + async def test_cred_defs_get(self, mock_get_cred_defs): + result = await test_module.cred_defs_get(self.request) + assert json.loads(result.body).__len__() == 2 + + result = await test_module.cred_defs_get(self.request) + assert json.loads(result.body).__len__() == 0 + + assert mock_get_cred_defs.call_count == 2 + + @mock.patch.object( + AnonCredsIssuer, + "match_created_credential_definitions", + side_effect=["found", None], + ) + @mock.patch.object( + AnonCredsRevocation, + "create_and_register_revocation_registry_definition", + return_value=MockRovocationRegistryDefinition("revRegId"), + ) + async def test_rev_reg_def_post(self, mock_match, mock_create): + self.request.json = mock.CoroutineMock( + return_value={ + "credDefId": "cred_def_id", + "issuerId": "issuer_id", + "maxCredNum": 100, + "options": { + "tails_public_uri": "http://tails_public_uri", + "tails_local_uri": "http://tails_local_uri", + }, + } + ) + + result = await test_module.rev_reg_def_post(self.request) + + assert ( + json.loads(result.body)["revocation_registry_definition_id"] == "revRegId" + ) + + assert mock_match.call_count == 1 + assert mock_create.call_count == 1 + + with self.assertRaises(web.HTTPNotFound): + await test_module.rev_reg_def_post(self.request) + + @mock.patch.object( + AnonCredsRevocation, + "create_and_register_revocation_list", + return_value=MockRovocationRegistryDefinition("revRegId"), + ) + async def test_rev_list_post(self, mock_create): + self.request.json = mock.CoroutineMock( + return_value={"revRegDefId": "rev_reg_def_id", "options": {}} + ) + result = await test_module.rev_list_post(self.request) + assert ( + json.loads(result.body)["revocation_registry_definition_id"] == "revRegId" + ) + assert mock_create.call_count == 1 + + @mock.patch.object( + AnonCredsRevocation, + "get_created_revocation_registry_definition", + side_effect=[ + MockRovocationRegistryDefinition("revRegId"), + None, + MockRovocationRegistryDefinition("revRegId"), + ], + ) + @mock.patch.object( + AnonCredsRevocation, + "upload_tails_file", + return_value=None, + ) + async def test_upload_tails_file(self, mock_upload, mock_get): + self.request.match_info = {"rev_reg_id": "rev_reg_id"} + result = await test_module.upload_tails_file(self.request) + assert result is not None + assert mock_upload.call_count == 1 + assert mock_get.call_count == 1 + + with self.assertRaises(web.HTTPNotFound): + await test_module.upload_tails_file(self.request) + + self.request.match_info = {} + + with self.assertRaises(KeyError): + await test_module.upload_tails_file(self.request) + + @mock.patch.object( + AnonCredsRevocation, + "set_active_registry", + return_value=None, + ) + async def test_set_active_registry(self, mock_set): + self.request.match_info = {"rev_reg_id": "rev_reg_id"} + await test_module.set_active_registry(self.request) + assert mock_set.call_count == 1 + + self.request.match_info = {} + with self.assertRaises(KeyError): + await test_module.set_active_registry(self.request) + + async def test_revoke_notify_without_connection_throws_x(self): + self.request.json = mock.CoroutineMock(return_value={"notify": True}) + with self.assertRaises(web.HTTPBadRequest): + await test_module.revoke(self.request) + + @mock.patch.object( + RevocationManager, + "revoke_credential_by_cred_ex_id", + return_value=None, + ) + @mock.patch.object( + RevocationManager, + "revoke_credential", + return_value=None, + ) + async def test_revoke(self, mock_revoke, mock_revoke_by_id): + self.request.json = mock.CoroutineMock( + return_value={"cred_ex_id": "cred_ex_id"} + ) + await test_module.revoke(self.request) + assert mock_revoke_by_id.call_count == 1 + assert mock_revoke.call_count == 0 + + self.request.json = mock.CoroutineMock(return_value={}) + await test_module.revoke(self.request) + assert mock_revoke.call_count == 1 + + @mock.patch.object( + RevocationManager, + "publish_pending_revocations", + return_value="test-rrid", + ) + async def test_publish_revocations(self, mock_publish): + self.request.json = mock.CoroutineMock(return_value={"rrid2crid": "rrid2crid"}) + result = await test_module.publish_revocations(self.request) + + assert json.loads(result.body)["rrid2crid"] == "test-rrid" + assert mock_publish.call_count == 1 + + @mock.patch.object(DefaultRevocationSetup, "register_events") + async def test_register_events(self, mock_manager): + test_module.register_events("event_bus") + mock_manager.assert_called_once_with("event_bus") + async def test_register(self): - mock_app = async_mock.MagicMock() - mock_app.add_routes = async_mock.MagicMock() + mock_app = mock.MagicMock() + mock_app.add_routes = mock.MagicMock() await test_module.register(mock_app) mock_app.add_routes.assert_called_once() async def test_post_process_routes(self): - mock_app = async_mock.MagicMock(_state={"swagger_dict": {}}) + mock_app = mock.MagicMock(_state={"swagger_dict": {}}) test_module.post_process_routes(mock_app) assert "tags" in mock_app._state["swagger_dict"] - - -""" -Include a test for each route: - -- schemas_post() -- schema_get() -- schemas_get() -- cred_def_post() -- cred_def_get() -- cred_defs_get() -- etc ... - -For example see unit tests for routes under: -- aries_cloudagent/messaging/schemas -- aries_cloudagent/messaging/credential_definitions -- etc ... -""" diff --git a/aries_cloudagent/anoncreds/tests/test_verifier.py b/aries_cloudagent/anoncreds/tests/test_verifier.py index 5df0a1ce98..773fa64847 100644 --- a/aries_cloudagent/anoncreds/tests/test_verifier.py +++ b/aries_cloudagent/anoncreds/tests/test_verifier.py @@ -1,7 +1,647 @@ -""" -Unit tests for the anoncreds verifier: +from copy import deepcopy -- request, receive and verify presentation +import pytest +from asynctest import TestCase -See tests under aries_cloudagent/indy/sdk/tests -""" +from aries_cloudagent.anoncreds.models.anoncreds_cred_def import ( + CredDef, + CredDefValue, + CredDefValuePrimary, + CredDefValueRevocation, + GetCredDefResult, +) +from aries_cloudagent.anoncreds.models.anoncreds_revocation import ( + GetRevListResult, + GetRevRegDefResult, + RevList, + RevRegDef, + RevRegDefValue, +) +from aries_cloudagent.anoncreds.models.anoncreds_schema import ( + AnonCredsSchema, + GetSchemaResult, +) +from aries_cloudagent.askar.profile_anon import AskarAnoncredsProfile +from aries_cloudagent.core.in_memory.profile import ( + InMemoryProfile, +) +from aries_cloudagent.tests import mock + +from .. import verifier as test_module +from .mock_objects import ( + MOCK_CRED_DEFS, + MOCK_PRES, + MOCK_PRES_REQ, + MOCK_REV_REG_DEFS, + MOCK_SCHEMAS, +) + + +@pytest.mark.anoncreds +class TestAnonCredsVerifier(TestCase): + def setUp(self) -> None: + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar-anoncreds"}, + profile_class=AskarAnoncredsProfile, + ) + self.verifier = test_module.AnonCredsVerifier(self.profile) + + async def test_init(self): + assert self.verifier.profile == self.profile + + async def test_non_revoc_intervals(self): + result = self.verifier.non_revoc_intervals( + MOCK_PRES_REQ, MOCK_PRES, MOCK_CRED_DEFS + ) + assert isinstance(result, list) + + non_revoked_req = deepcopy(MOCK_PRES_REQ) + non_revoked_req["requested_attributes"]["biomarker_attrs_0"]["non_revoked"] = ( + { + "from": 1593922800, + "to": 1593922800, + }, + ) + + result = self.verifier.non_revoc_intervals( + non_revoked_req, MOCK_PRES, MOCK_CRED_DEFS + ) + + async def test_check_timestamps_with_names(self): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential_definition=mock.CoroutineMock( + return_value=GetCredDefResult( + credential_definition_id="TUku9MDGa7QALbAJX4oAww:3:CL:531757:MYCO_Consent_Enablement", + credential_definition=CredDef( + issuer_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z") + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ) + ) + ) + ) + await self.verifier.check_timestamps( + self.profile, MOCK_PRES_REQ, MOCK_PRES, MOCK_REV_REG_DEFS + ) + + # irrevocable cred-def with timestamp + mock_pres = deepcopy(MOCK_PRES) + mock_pres["identifiers"][0]["timestamp"] = 9999999999 + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, MOCK_PRES_REQ, mock_pres, MOCK_REV_REG_DEFS + ) + + # revocable cred-def with timestamp + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential_definition=mock.CoroutineMock( + return_value=GetCredDefResult( + credential_definition_id="TUku9MDGa7QALbAJX4oAww:3:CL:531757:MYCO_Consent_Enablement", + credential_definition=CredDef( + issuer_id="issuer-id", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z"), + revocation=CredDefValueRevocation( + g="g", + g_dash="g_dash", + h="h", + h0="h0", + h1="h1", + h2="h2", + h_cap="h_cap", + htilde="htilde", + pk="pk", + u="u", + y="y", + ), + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ) + ) + ) + ) + + # too far in future + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, MOCK_PRES_REQ, mock_pres, MOCK_REV_REG_DEFS + ) + + # no rev_reg_id + mock_pres["identifiers"][0]["timestamp"] = 1234567890 + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, MOCK_PRES_REQ, mock_pres, MOCK_REV_REG_DEFS + ) + + # with rev_reg_id + mock_pres["identifiers"][0][ + "rev_reg_id" + ] = "TUku9MDGa7QALbAJX4oAww:3:TUku9MDGa7QALbAJX4oAww:3:CL:18:tag:CL_ACCUM:0" + + # Superfluous timestamp + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, MOCK_PRES_REQ, mock_pres, MOCK_REV_REG_DEFS + ) + + # Valid + mock_pres["identifiers"][0]["timestamp"] = None + await self.verifier.check_timestamps( + self.profile, MOCK_PRES_REQ, mock_pres, MOCK_REV_REG_DEFS + ) + + # outside of non-revocation interval + mock_pres_req = deepcopy(MOCK_PRES_REQ) + mock_pres_req["requested_attributes"]["biomarker_attrs_0"]["non_revoked"] = { + "from": 9000000000, + "to": 1000000000, + } + mock_pres["identifiers"][0]["timestamp"] = 123456789 + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + # no revealed attr groups + mock_pres["requested_proof"]["revealed_attr_groups"] = {} + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + async def test_check_timestamps_with_name(self): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential_definition=mock.CoroutineMock( + return_value=GetCredDefResult( + credential_definition_id="TUku9MDGa7QALbAJX4oAww:3:CL:531757:MYCO_Consent_Enablement", + credential_definition=CredDef( + issuer_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z") + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ) + ) + ) + ) + mock_pres_req = deepcopy(MOCK_PRES_REQ) + mock_pres = deepcopy(MOCK_PRES) + del mock_pres_req["requested_attributes"]["biomarker_attrs_0"]["names"] + mock_pres_req["requested_attributes"]["biomarker_attrs_0"]["name"] = "name" + + # invalid group + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + # valid + del mock_pres["requested_proof"]["revealed_attrs"]["consent_attrs"] + del mock_pres["requested_proof"]["revealed_attr_groups"]["biomarker_attrs_0"] + del mock_pres_req["requested_attributes"]["consent_attrs"] + mock_pres["requested_proof"]["revealed_attrs"]["biomarker_attrs_0"] = { + "sub_proof_index": 0, + "raw": "Iron", + "encoded": "85547618788485118809771015708850341281587970912661276233439574555663751388073", + } + + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + # revocable cred-def with timestamp + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential_definition=mock.CoroutineMock( + return_value=GetCredDefResult( + credential_definition_id="TUku9MDGa7QALbAJX4oAww:3:CL:531757:MYCO_Consent_Enablement", + credential_definition=CredDef( + issuer_id="issuer-id", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z"), + revocation=CredDefValueRevocation( + g="g", + g_dash="g_dash", + h="h", + h0="h0", + h1="h1", + h2="h2", + h_cap="h_cap", + htilde="htilde", + pk="pk", + u="u", + y="y", + ), + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ) + ) + ) + ) + + # no rev_reg_id + mock_pres["identifiers"][0]["timestamp"] = 1234567890 + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + # with rev_reg_id + mock_pres["identifiers"][0][ + "rev_reg_id" + ] = "TUku9MDGa7QALbAJX4oAww:3:TUku9MDGa7QALbAJX4oAww:3:CL:18:tag:CL_ACCUM:0" + + # Superfluous timestamp + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + # outside of non-revocation interval + mock_pres_req["requested_attributes"]["biomarker_attrs_0"]["non_revoked"] = { + "from": 9000000000, + "to": 1000000000, + } + mock_pres["identifiers"][0]["timestamp"] = 123456789 + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + # unrevealed attr + mock_pres["requested_proof"]["revealed_attrs"] = {} + mock_pres["requested_proof"]["unrevealed_attrs"]["biomarker_attrs_0"] = { + "sub_proof_index": 0, + } + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + async def test_check_timestamps_predicates(self): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential_definition=mock.CoroutineMock( + return_value=GetCredDefResult( + credential_definition_id="TUku9MDGa7QALbAJX4oAww:3:CL:531757:MYCO_Consent_Enablement", + credential_definition=CredDef( + issuer_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z") + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ) + ) + ) + ) + + mock_pres_req = deepcopy(MOCK_PRES_REQ) + mock_pres_req["requested_attributes"] = {} + mock_pres_req["requested_predicates"]["0_concentration_GE_uuid"] = { + "name": "concentration", + "p_type": "<=", + "p_value": 9, + "restrictions": [{"schema_name": "MYCO Biomarker"}], + } + + # predicate not in proof + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, mock_pres_req, MOCK_PRES, MOCK_REV_REG_DEFS + ) + + mock_pres = deepcopy(MOCK_PRES) + mock_pres["requested_proof"]["predicates"] = { + "0_concentration_GE_uuid": {"sub_proof_index": 0} + } + + # valid - no revocation + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + # revocation + + # revocable cred-def with timestamp + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential_definition=mock.CoroutineMock( + return_value=GetCredDefResult( + credential_definition_id="TUku9MDGa7QALbAJX4oAww:3:CL:531757:MYCO_Consent_Enablement", + credential_definition=CredDef( + issuer_id="issuer-id", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z"), + revocation=CredDefValueRevocation( + g="g", + g_dash="g_dash", + h="h", + h0="h0", + h1="h1", + h2="h2", + h_cap="h_cap", + htilde="htilde", + pk="pk", + u="u", + y="y", + ), + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ) + ) + ) + ) + + # no rev_reg_id + mock_pres["identifiers"][0]["timestamp"] = 1234567890 + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + # with rev_reg_id + mock_pres["identifiers"][0][ + "rev_reg_id" + ] = "TUku9MDGa7QALbAJX4oAww:3:TUku9MDGa7QALbAJX4oAww:3:CL:18:tag:CL_ACCUM:0" + + # Superfluous timestamp + with self.assertRaises(ValueError): + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + # outside of non-revocation interval + mock_pres_req["requested_predicates"]["0_concentration_GE_uuid"][ + "non_revoked" + ] = { + "from": 9000000000, + "to": 1000000000, + } + mock_pres["identifiers"][0]["timestamp"] = 123456789 + await self.verifier.check_timestamps( + self.profile, mock_pres_req, mock_pres, MOCK_REV_REG_DEFS + ) + + async def test_pre_verify_incomplete_objects(self): + mock_pres_req = deepcopy(MOCK_PRES_REQ) + mock_pres = deepcopy(MOCK_PRES) + + with self.assertRaises(ValueError): + await self.verifier.pre_verify(mock_pres_req, {}) + + with self.assertRaises(ValueError): + await self.verifier.pre_verify({}, mock_pres) + + del mock_pres_req["requested_predicates"] + with self.assertRaises(ValueError): + await self.verifier.pre_verify(mock_pres_req, mock_pres) + mock_pres_req["requested_predicates"] = {} + + del mock_pres_req["requested_attributes"] + with self.assertRaises(ValueError): + await self.verifier.pre_verify(mock_pres_req, mock_pres) + mock_pres_req["requested_attributes"] = {} + + del mock_pres["requested_proof"] + with self.assertRaises(ValueError): + await self.verifier.pre_verify(mock_pres_req, mock_pres) + mock_pres["requested_proof"] = {} + + del mock_pres["proof"] + with self.assertRaises(ValueError): + await self.verifier.pre_verify(mock_pres_req, mock_pres) + + async def test_pre_verify(self): + mock_pres_req = deepcopy(MOCK_PRES_REQ) + mock_pres = deepcopy(MOCK_PRES) + + # valid - with names and name + result = await self.verifier.pre_verify(mock_pres_req, mock_pres) + assert isinstance(result, list) + + # name in unrevealed attrs + mock_pres["requested_proof"]["revealed_attrs"] = {} + mock_pres["requested_proof"]["unrevealed_attrs"] = { + "consent_attrs": {"sub_proof_index": 1} + } + await self.verifier.pre_verify(mock_pres_req, mock_pres) + + # self-attested attrs with restrictions + mock_pres["requested_proof"]["unrevealed_attrs"] = {} + mock_pres["requested_proof"]["self_attested_attrs"] = { + "consent_attrs": "I agree to share my data with the verifier" + } + with self.assertRaises(ValueError): + await self.verifier.pre_verify(mock_pres_req, mock_pres) + + # valid - self-attested + mock_pres_req["requested_attributes"]["consent_attrs"]["restrictions"] = [] + await self.verifier.pre_verify(mock_pres_req, mock_pres) + + # no name or names + del mock_pres_req["requested_attributes"]["consent_attrs"]["name"] + with self.assertRaises(ValueError): + await self.verifier.pre_verify(mock_pres_req, mock_pres) + mock_pres_req["requested_attributes"]["consent_attrs"][ + "name" + ] = "jti_unique_identifier" + # attr not in proof + mock_pres["requested_proof"]["self_attested_attrs"] = {} + with self.assertRaises(ValueError): + await self.verifier.pre_verify(mock_pres_req, mock_pres) + + # predicates + + # predicate not in proof + mock_pres_req["requested_attributes"] = {} + mock_pres_req["requested_predicates"]["0_concentration_GE_uuid"] = { + "name": "concentration", + "p_type": "<=", + "p_value": 9, + "restrictions": [{"schema_name": "MYCO Biomarker"}], + } + + mock_pres["requested_proof"]["predicates"] = { + "0_concentration_GE_uuid": {"sub_proof_index": 0} + } + with self.assertRaises(ValueError): + await self.verifier.pre_verify(mock_pres_req, mock_pres) + + mock_pres["proof"]["proofs"][0]["primary_proof"]["ge_proofs"] = [ + { + "predicate": { + "attr_name": "concentration", + "p_type": "<=", + "value": 9, + }, + } + ] + await self.verifier.pre_verify(mock_pres_req, mock_pres) + + async def test_process_pres_identifiers(self): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_schema=mock.CoroutineMock( + return_value=GetSchemaResult( + schema_id="schema-id", + schema=AnonCredsSchema( + issuer_id="issuer-id", + name="schema-name", + version="1.0", + attr_names=["attr1", "attr2"], + ), + schema_metadata={}, + resolution_metadata={}, + ) + ), + get_credential_definition=mock.CoroutineMock( + return_value=GetCredDefResult( + credential_definition_id="cred-def-id", + credential_definition=CredDef( + issuer_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z") + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ) + ), + get_revocation_registry_definition=mock.CoroutineMock( + return_value=GetRevRegDefResult( + revocation_registry_id="rev-reg-id", + revocation_registry=RevRegDef( + issuer_id="issuer-id", + cred_def_id="cred-def-id", + type="CL_ACCUM", + tag="tag", + value=RevRegDefValue( + public_keys={}, + max_cred_num=1000, + tails_hash="tails-hash", + tails_location="tails-location", + ), + ), + resolution_metadata={}, + revocation_registry_metadata={}, + ) + ), + get_revocation_list=mock.CoroutineMock( + return_value=GetRevListResult( + revocation_list=RevList( + issuer_id="issuer-id", + rev_reg_def_id="rev-reg-def-id", + revocation_list=[], + current_accumulator="current-accumulator", + ), + resolution_metadata={}, + revocation_registry_metadata={}, + ) + ), + ) + ) + result = await self.verifier.process_pres_identifiers( + [ + { + "schema_id": "schema-id", + "cred_def_id": "cred-def-id", + "rev_reg_id": "rev-reg-id", + "timestamp": 1234567890, + } + ] + ) + + assert isinstance(result, tuple) + assert len(result) == 4 + + async def test_verify_presentation(self): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential_definition=mock.CoroutineMock( + return_value=GetCredDefResult( + credential_definition_id="cred-def-id", + credential_definition=CredDef( + issuer_id="did:indy:sovrin:SGrjRL82Y9ZZbzhUDXokvQ", + schema_id="schema-id", + tag="tag", + type="CL", + value=CredDefValue( + primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z") + ), + ), + credential_definition_metadata={}, + resolution_metadata={}, + ) + ) + ) + ) + result = await self.verifier.verify_presentation( + pres_req=MOCK_PRES_REQ, + pres=MOCK_PRES, + schemas=MOCK_SCHEMAS, + credential_definitions=MOCK_CRED_DEFS, + rev_reg_defs=[], + rev_lists={}, + ) + + assert isinstance(result, tuple) + assert len(result) == 2 + assert result[0] is False + + @mock.patch.object( + test_module.AnonCredsVerifier, "pre_verify", side_effect=ValueError() + ) + async def test_verify_presentation_value_error_caught(self, mock_verify): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential_definition=mock.CoroutineMock( + side_effect=ValueError("Bad credential definition") + ) + ) + ) + (result, msgs) = await self.verifier.verify_presentation( + pres_req=MOCK_PRES_REQ, + pres=MOCK_PRES, + schemas=MOCK_SCHEMAS, + credential_definitions=MOCK_CRED_DEFS, + rev_reg_defs=[], + rev_lists={}, + ) + assert result is False + assert isinstance(msgs, list) diff --git a/aries_cloudagent/anoncreds/verifier.py b/aries_cloudagent/anoncreds/verifier.py index ee29fdd83d..f320d82013 100644 --- a/aries_cloudagent/anoncreds/verifier.py +++ b/aries_cloudagent/anoncreds/verifier.py @@ -8,11 +8,11 @@ from anoncreds import AnoncredsError, Presentation -from .registry import AnonCredsRegistry -from .models.anoncreds_cred_def import GetCredDefResult -from ..indy.models.xform import indy_proof_req2non_revoc_intervals from ..core.profile import Profile +from ..indy.models.xform import indy_proof_req2non_revoc_intervals from ..messaging.util import canon, encode +from .models.anoncreds_cred_def import GetCredDefResult +from .registry import AnonCredsRegistry LOGGER = logging.getLogger(__name__) @@ -304,6 +304,7 @@ async def pre_verify(self, pres_req: dict, pres: dict) -> list: canon_attr = canon(req_pred["name"]) matched = False found = False + pred = None for ge_proof in pres["proof"]["proofs"][ pres["requested_proof"]["predicates"][uuid]["sub_proof_index"] ]["primary_proof"]["ge_proofs"]: @@ -314,8 +315,7 @@ async def pre_verify(self, pres_req: dict, pres: dict) -> list: matched = True break if not matched: - raise ValueError(f"Predicate value != p_value: {pred['attr_name']}") - break + raise ValueError(f"Predicate not found: {canon_attr}") elif not found: raise ValueError(f"Missing requested predicate '{uuid}'") except (KeyError, TypeError): diff --git a/aries_cloudagent/core/in_memory/profile.py b/aries_cloudagent/core/in_memory/profile.py index 785457685e..42b2ad6374 100644 --- a/aries_cloudagent/core/in_memory/profile.py +++ b/aries_cloudagent/core/in_memory/profile.py @@ -1,9 +1,9 @@ """Manage in-memory profile interaction.""" - from collections import OrderedDict from typing import Any, Mapping, Type from weakref import ref +from aries_askar import Session from ...config.injection_context import InjectionContext from ...config.provider import ClassProvider @@ -11,7 +11,6 @@ from ...storage.vc_holder.base import VCHolder from ...utils.classloader import DeferLoad from ...wallet.base import BaseWallet - from ..profile import Profile, ProfileManager, ProfileSession STORAGE_CLASS = DeferLoad("aries_cloudagent.storage.in_memory.InMemoryStorage") @@ -129,6 +128,7 @@ async def _setup(self): """Create the session or transaction connection, if needed.""" await super()._setup() self._init_context() + self._handle: Session = None def _init_context(self): """Initialize the session context.""" @@ -137,6 +137,11 @@ def _init_context(self): ) self._context.injector.bind_instance(BaseWallet, WALLET_CLASS(self.profile)) + @property + def handle(self) -> Session: + """Accessor for the Session instance.""" + return self._handle + @property def storage(self) -> BaseStorage: """Get the `BaseStorage` implementation (helper specific to in-memory profile).""" diff --git a/pyproject.toml b/pyproject.toml index a7106e4ca5..a031ebdfc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,6 +127,7 @@ addopts = """ --ruff """ markers = [ + "anoncreds: Tests specifically relating to AnonCreds support", "askar: Tests specifically relating to Aries-Askar support", "indy: Tests specifically relating to Hyperledger Indy SDK support", "indy_credx: Tests specifically relating to Indy-Credx support",