From ef8004a140c69e7780e73f31bfd77118a05c1852 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Thu, 2 Sep 2021 16:30:14 -0700 Subject: [PATCH 1/9] Refactoring in prep for adding connectionless proof demo Signed-off-by: Ian Costanzo --- demo/runners/faber.py | 605 ++++++++++++++++++++++++------------------ 1 file changed, 341 insertions(+), 264 deletions(-) diff --git a/demo/runners/faber.py b/demo/runners/faber.py index dcb14d8095..ac68a11e3a 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -67,6 +67,293 @@ async def detect_connection(self): def connection_ready(self): return self._connection_ready.done() and self._connection_ready.result() + def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracing): + if aip == 10: + # define attributes to send for credential + self.cred_attrs[cred_def_id] = { + "name": "Alice Smith", + "date": "2018-05-28", + "degree": "Maths", + "age": "24", + "timestamp": str(int(time.time())), + } + + cred_preview = { + "@type": CRED_PREVIEW_TYPE, + "attributes": [ + {"name": n, "value": v} + for (n, v) in self.cred_attrs[cred_def_id].items() + ], + } + offer_request = { + "connection_id": self.connection_id, + "cred_def_id": cred_def_id, + "comment": f"Offer on cred def id {cred_def_id}", + "auto_remove": False, + "credential_preview": cred_preview, + "trace": exchange_tracing, + } + return offer_request + + elif aip == 20: + if cred_type == CRED_FORMAT_INDY: + self.cred_attrs[cred_def_id] = { + "name": "Alice Smith", + "date": "2018-05-28", + "degree": "Maths", + "age": "24", + "timestamp": str(int(time.time())), + } + + cred_preview = { + "@type": CRED_PREVIEW_TYPE, + "attributes": [ + {"name": n, "value": v} + for (n, v) in self.cred_attrs[cred_def_id].items() + ], + } + offer_request = { + "connection_id": self.connection_id, + "comment": f"Offer on cred def id {cred_def_id}", + "auto_remove": False, + "credential_preview": cred_preview, + "filter": {"indy": {"cred_def_id": cred_def_id}}, + "trace": exchange_tracing, + } + return offer_request + + elif cred_type == CRED_FORMAT_JSON_LD: + offer_request = { + "connection_id": self.connection_id, + "filter": { + "ld_proof": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/citizenship/v1", + ], + "type": [ + "VerifiableCredential", + "PermanentResident", + ], + "id": "https://credential.example.com/residents/1234567890", + "issuer": self.did, + "issuanceDate": "2020-01-01T12:00:00Z", + "credentialSubject": { + "type": ["PermanentResident"], + # "id": "", + "givenName": "ALICE", + "familyName": "SMITH", + "gender": "Female", + "birthCountry": "Bahamas", + "birthDate": "1958-07-17", + }, + }, + "options": {"proofType": SIG_TYPE_BLS}, + } + }, + } + return offer_request + + else: + raise Exception(f"Error invalid credential type: {self.cred_type}") + + else: + raise Exception(f"Error invalid AIP level: {self.aip}") + + def generate_proof_request_web_request( + self, aip, cred_type, revocation, exchange_tracing + ): + if aip == 10: + req_attrs = [ + { + "name": "name", + "restrictions": [{"schema_name": "degree schema"}], + }, + { + "name": "date", + "restrictions": [{"schema_name": "degree schema"}], + }, + ] + if revocation: + req_attrs.append( + { + "name": "degree", + "restrictions": [{"schema_name": "degree schema"}], + "non_revoked": {"to": int(time.time() - 1)}, + }, + ) + else: + req_attrs.append( + { + "name": "degree", + "restrictions": [{"schema_name": "degree schema"}], + } + ) + if SELF_ATTESTED: + # test self-attested claims + req_attrs.append( + {"name": "self_attested_thing"}, + ) + req_preds = [ + # test zero-knowledge proofs + { + "name": "age", + "p_type": ">=", + "p_value": 18, + "restrictions": [{"schema_name": "degree schema"}], + } + ] + indy_proof_request = { + "name": "Proof of Education", + "version": "1.0", + "requested_attributes": { + f"0_{req_attr['name']}_uuid": req_attr for req_attr in req_attrs + }, + "requested_predicates": { + f"0_{req_pred['name']}_GE_uuid": req_pred for req_pred in req_preds + }, + } + + if revocation: + indy_proof_request["non_revoked"] = {"to": int(time.time())} + + proof_request_web_request = { + "connection_id": self.connection_id, + "proof_request": indy_proof_request, + "trace": exchange_tracing, + } + return proof_request_web_request + + elif aip == 20: + if cred_type == CRED_FORMAT_INDY: + req_attrs = [ + { + "name": "name", + "restrictions": [{"schema_name": "degree schema"}], + }, + { + "name": "date", + "restrictions": [{"schema_name": "degree schema"}], + }, + ] + if revocation: + req_attrs.append( + { + "name": "degree", + "restrictions": [{"schema_name": "degree schema"}], + "non_revoked": {"to": int(time.time() - 1)}, + }, + ) + else: + req_attrs.append( + { + "name": "degree", + "restrictions": [{"schema_name": "degree schema"}], + } + ) + if SELF_ATTESTED: + # test self-attested claims + req_attrs.append( + {"name": "self_attested_thing"}, + ) + req_preds = [ + # test zero-knowledge proofs + { + "name": "age", + "p_type": ">=", + "p_value": 18, + "restrictions": [{"schema_name": "degree schema"}], + } + ] + indy_proof_request = { + "name": "Proof of Education", + "version": "1.0", + "requested_attributes": { + f"0_{req_attr['name']}_uuid": req_attr for req_attr in req_attrs + }, + "requested_predicates": { + f"0_{req_pred['name']}_GE_uuid": req_pred + for req_pred in req_preds + }, + } + + if revocation: + indy_proof_request["non_revoked"] = {"to": int(time.time())} + + proof_request_web_request = { + "connection_id": self.connection_id, + "presentation_request": {"indy": indy_proof_request}, + "trace": exchange_tracing, + } + return proof_request_web_request + + elif cred_type == CRED_FORMAT_JSON_LD: + proof_request_web_request = { + "comment": "test proof request for json-ld", + "connection_id": self.connection_id, + "presentation_request": { + "dif": { + "options": { + "challenge": "3fa85f64-5717-4562-b3fc-2c963f66afa7", + "domain": "4jt78h47fh47", + }, + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0654", + "format": {"ldp_vp": {"proof_type": [SIG_TYPE_BLS]}}, + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" + }, + { + "uri": "https://w3id.org/citizenship#PermanentResident" + }, + ], + "constraints": { + "limit_disclosure": "required", + "is_holder": [ + { + "directive": "required", + "field_id": [ + "1f44d55f-f161-4938-a659-f8026467f126" + ], + } + ], + "fields": [ + { + "id": "1f44d55f-f161-4938-a659-f8026467f126", + "path": [ + "$.credentialSubject.familyName" + ], + "purpose": "The claim must be from one of the specified person", + "filter": {"const": "SMITH"}, + }, + { + "path": [ + "$.credentialSubject.givenName" + ], + "purpose": "The claim must be from one of the specified person", + }, + ], + }, + } + ], + }, + } + }, + } + return proof_request_web_request + + else: + raise Exception(f"Error invalid credential type: {self.cred_type}") + + else: + raise Exception(f"Error invalid AIP level: {self.aip}") + async def main(args): faber_agent = await create_agent_with_args(args, ident="faber") @@ -116,6 +403,7 @@ async def main(args): options = ( " (1) Issue Credential\n" " (2) Send Proof Request\n" + " (2a) Send *Connectionless* Proof Request (requires a Mobile client)\n" " (3) Send Message\n" " (4) Create New Invitation\n" ) @@ -174,97 +462,29 @@ async def main(args): log_status("#13 Issue credential offer to X") if faber_agent.aip == 10: - # define attributes to send for credential - faber_agent.agent.cred_attrs[faber_agent.cred_def_id] = { - "name": "Alice Smith", - "date": "2018-05-28", - "degree": "Maths", - "age": "24", - "timestamp": str(int(time.time())), - } - - cred_preview = { - "@type": CRED_PREVIEW_TYPE, - "attributes": [ - {"name": n, "value": v} - for (n, v) in faber_agent.agent.cred_attrs[ - faber_agent.cred_def_id - ].items() - ], - } - offer_request = { - "connection_id": faber_agent.agent.connection_id, - "cred_def_id": faber_agent.cred_def_id, - "comment": f"Offer on cred def id {faber_agent.cred_def_id}", - "auto_remove": False, - "credential_preview": cred_preview, - "trace": exchange_tracing, - } + offer_request = faber_agent.agent.generate_credential_offer( + faber_agent.aip, None, faber_agent.cred_def_id, exchange_tracing + ) await faber_agent.agent.admin_POST( "/issue-credential/send-offer", offer_request ) elif faber_agent.aip == 20: if faber_agent.cred_type == CRED_FORMAT_INDY: - faber_agent.agent.cred_attrs[faber_agent.cred_def_id] = { - "name": "Alice Smith", - "date": "2018-05-28", - "degree": "Maths", - "age": "24", - "timestamp": str(int(time.time())), - } - - cred_preview = { - "@type": CRED_PREVIEW_TYPE, - "attributes": [ - {"name": n, "value": v} - for (n, v) in faber_agent.agent.cred_attrs[ - faber_agent.cred_def_id - ].items() - ], - } - offer_request = { - "connection_id": faber_agent.agent.connection_id, - "comment": f"Offer on cred def id {faber_agent.cred_def_id}", - "auto_remove": False, - "credential_preview": cred_preview, - "filter": { - "indy": {"cred_def_id": faber_agent.cred_def_id} - }, - "trace": exchange_tracing, - } + offer_request = faber_agent.agent.generate_credential_offer( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.cred_def_id, + exchange_tracing, + ) elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: - offer_request = { - "connection_id": faber_agent.agent.connection_id, - "filter": { - "ld_proof": { - "credential": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/citizenship/v1", - ], - "type": [ - "VerifiableCredential", - "PermanentResident", - ], - "id": "https://credential.example.com/residents/1234567890", - "issuer": faber_agent.agent.did, - "issuanceDate": "2020-01-01T12:00:00Z", - "credentialSubject": { - "type": ["PermanentResident"], - # "id": "", - "givenName": "ALICE", - "familyName": "SMITH", - "gender": "Female", - "birthCountry": "Bahamas", - "birthDate": "1958-07-17", - }, - }, - "options": {"proofType": SIG_TYPE_BLS}, - } - }, - } + offer_request = faber_agent.agent.generate_credential_offer( + faber_agent.aip, + faber_agent.cred_type, + None, + exchange_tracing, + ) else: raise Exception( @@ -281,65 +501,14 @@ async def main(args): elif option == "2": log_status("#20 Request proof of degree from alice") if faber_agent.aip == 10: - req_attrs = [ - { - "name": "name", - "restrictions": [{"schema_name": "degree schema"}], - }, - { - "name": "date", - "restrictions": [{"schema_name": "degree schema"}], - }, - ] - if faber_agent.revocation: - req_attrs.append( - { - "name": "degree", - "restrictions": [{"schema_name": "degree schema"}], - "non_revoked": {"to": int(time.time() - 1)}, - }, - ) - else: - req_attrs.append( - { - "name": "degree", - "restrictions": [{"schema_name": "degree schema"}], - } + proof_request_web_request = ( + faber_agent.agent.generate_proof_request_web_request( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.revocation, + exchange_tracing, ) - if SELF_ATTESTED: - # test self-attested claims - req_attrs.append( - {"name": "self_attested_thing"}, - ) - req_preds = [ - # test zero-knowledge proofs - { - "name": "age", - "p_type": ">=", - "p_value": 18, - "restrictions": [{"schema_name": "degree schema"}], - } - ] - indy_proof_request = { - "name": "Proof of Education", - "version": "1.0", - "requested_attributes": { - f"0_{req_attr['name']}_uuid": req_attr - for req_attr in req_attrs - }, - "requested_predicates": { - f"0_{req_pred['name']}_GE_uuid": req_pred - for req_pred in req_preds - }, - } - - if faber_agent.revocation: - indy_proof_request["non_revoked"] = {"to": int(time.time())} - proof_request_web_request = { - "connection_id": faber_agent.agent.connection_id, - "proof_request": indy_proof_request, - "trace": exchange_tracing, - } + ) await faber_agent.agent.admin_POST( "/present-proof/send-request", proof_request_web_request ) @@ -347,132 +516,24 @@ async def main(args): elif faber_agent.aip == 20: if faber_agent.cred_type == CRED_FORMAT_INDY: - req_attrs = [ - { - "name": "name", - "restrictions": [{"schema_name": faber_schema_name}], - }, - { - "name": "date", - "restrictions": [{"schema_name": faber_schema_name}], - }, - ] - if faber_agent.revocation: - req_attrs.append( - { - "name": "degree", - "restrictions": [ - {"schema_name": faber_schema_name} - ], - "non_revoked": {"to": int(time.time() - 1)}, - }, + proof_request_web_request = ( + faber_agent.agent.generate_proof_request_web_request( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.revocation, + exchange_tracing, ) - else: - req_attrs.append( - { - "name": "degree", - "restrictions": [ - {"schema_name": faber_schema_name} - ], - } - ) - if SELF_ATTESTED: - # test self-attested claims - req_attrs.append( - {"name": "self_attested_thing"}, - ) - req_preds = [ - # test zero-knowledge proofs - { - "name": "age", - "p_type": ">=", - "p_value": 18, - "restrictions": [{"schema_name": faber_schema_name}], - } - ] - indy_proof_request = { - "name": "Proof of Education", - "version": "1.0", - "requested_attributes": { - f"0_{req_attr['name']}_uuid": req_attr - for req_attr in req_attrs - }, - "requested_predicates": { - f"0_{req_pred['name']}_GE_uuid": req_pred - for req_pred in req_preds - }, - } - - if faber_agent.revocation: - indy_proof_request["non_revoked"] = {"to": int(time.time())} - proof_request_web_request = { - "connection_id": faber_agent.agent.connection_id, - "presentation_request": {"indy": indy_proof_request}, - "trace": exchange_tracing, - } + ) elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: - proof_request_web_request = { - "comment": "test proof request for json-ld", - "connection_id": faber_agent.agent.connection_id, - "presentation_request": { - "dif": { - "options": { - "challenge": "3fa85f64-5717-4562-b3fc-2c963f66afa7", - "domain": "4jt78h47fh47", - }, - "presentation_definition": { - "id": "32f54163-7166-48f1-93d8-ff217bdb0654", - "format": { - "ldp_vp": {"proof_type": [SIG_TYPE_BLS]} - }, - "input_descriptors": [ - { - "id": "citizenship_input_1", - "name": "EU Driver's License", - "schema": [ - { - "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" - }, - { - "uri": "https://w3id.org/citizenship#PermanentResident" - }, - ], - "constraints": { - "limit_disclosure": "required", - "is_holder": [ - { - "directive": "required", - "field_id": [ - "1f44d55f-f161-4938-a659-f8026467f126" - ], - } - ], - "fields": [ - { - "id": "1f44d55f-f161-4938-a659-f8026467f126", - "path": [ - "$.credentialSubject.familyName" - ], - "purpose": "The claim must be from one of the specified person", - "filter": { - "const": "SMITH" - }, - }, - { - "path": [ - "$.credentialSubject.givenName" - ], - "purpose": "The claim must be from one of the specified person", - }, - ], - }, - } - ], - }, - } - }, - } + proof_request_web_request = ( + faber_agent.agent.generate_proof_request_web_request( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.revocation, + exchange_tracing, + ) + ) else: raise Exception( @@ -486,6 +547,22 @@ async def main(args): else: raise Exception(f"Error invalid AIP level: {faber_agent.aip}") + elif option == "2a": + log_status("#20 Request * Connectionless * proof of degree from alice") + if faber_agent.aip == 10: + pass + elif faber_agent.aip == 20: + if faber_agent.cred_type == CRED_FORMAT_INDY: + pass + elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: + pass + else: + raise Exception( + "Error invalid credential type:" + faber_agent.cred_type + ) + else: + raise Exception(f"Error invalid AIP level: {faber_agent.aip}") + elif option == "3": msg = await prompt("Enter message: ") await faber_agent.agent.admin_POST( From 89426abd2fdbea29e98ed4310d4cdc1d8bcb5da7 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Fri, 3 Sep 2021 15:47:03 -0700 Subject: [PATCH 2/9] Add connectionless proof request support Signed-off-by: Ian Costanzo --- demo/runners/faber.py | 72 +++++++++++++++++++++++++++++++---- demo/runners/support/agent.py | 42 +++++++++++++++++++- 2 files changed, 106 insertions(+), 8 deletions(-) diff --git a/demo/runners/faber.py b/demo/runners/faber.py index ac68a11e3a..887735fac5 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -6,6 +6,7 @@ import time from aiohttp import ClientError +from qrcode import QRCode sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -162,7 +163,7 @@ def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracin raise Exception(f"Error invalid AIP level: {self.aip}") def generate_proof_request_web_request( - self, aip, cred_type, revocation, exchange_tracing + self, aip, cred_type, revocation, exchange_tracing, connectionless=False ): if aip == 10: req_attrs = [ @@ -219,10 +220,11 @@ def generate_proof_request_web_request( indy_proof_request["non_revoked"] = {"to": int(time.time())} proof_request_web_request = { - "connection_id": self.connection_id, "proof_request": indy_proof_request, "trace": exchange_tracing, } + if not connectionless: + proof_request_web_request["connection_id"] = self.connection_id return proof_request_web_request elif aip == 20: @@ -282,16 +284,16 @@ def generate_proof_request_web_request( indy_proof_request["non_revoked"] = {"to": int(time.time())} proof_request_web_request = { - "connection_id": self.connection_id, "presentation_request": {"indy": indy_proof_request}, "trace": exchange_tracing, } + if not connectionless: + proof_request_web_request["connection_id"] = self.connection_id return proof_request_web_request elif cred_type == CRED_FORMAT_JSON_LD: proof_request_web_request = { "comment": "test proof request for json-ld", - "connection_id": self.connection_id, "presentation_request": { "dif": { "options": { @@ -346,6 +348,8 @@ def generate_proof_request_web_request( } }, } + if not connectionless: + proof_request_web_request["connection_id"] = self.connection_id return proof_request_web_request else: @@ -550,16 +554,70 @@ async def main(args): elif option == "2a": log_status("#20 Request * Connectionless * proof of degree from alice") if faber_agent.aip == 10: - pass + proof_request_web_request = ( + faber_agent.agent.generate_proof_request_web_request( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.revocation, + exchange_tracing, + connectionless=True, + ) + ) + proof_request = await faber_agent.agent.admin_POST( + "/present-proof/create-request", proof_request_web_request + ) + pres_req_id = proof_request["presentation_exchange_id"] + url = ( + faber_agent.agent.webhook_url + "/pres_req/" + pres_req_id + "/" + ) + # TODO handle connectionless PR + qr = QRCode(border=1) + qr.add_data(url) + log_msg( + "Use the following JSON to accept the proof request from another demo agent." + ) + qr.print_ascii(invert=True) + elif faber_agent.aip == 20: if faber_agent.cred_type == CRED_FORMAT_INDY: - pass + proof_request_web_request = ( + faber_agent.agent.generate_proof_request_web_request( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.revocation, + exchange_tracing, + connectionless=True, + ) + ) elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: - pass + proof_request_web_request = ( + faber_agent.agent.generate_proof_request_web_request( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.revocation, + exchange_tracing, + connectionless=True, + ) + ) else: raise Exception( "Error invalid credential type:" + faber_agent.cred_type ) + + proof_request = await faber_agent.agent.admin_POST( + "/present-proof-2.0/create-request", proof_request_web_request + ) + pres_req_id = proof_request["pres_ex_id"] + url = ( + faber_agent.agent.webhook_url + "/pres_req/" + pres_req_id + "/" + ) + # TODO handle connectionless PR + qr = QRCode(border=1) + qr.add_data(url) + log_msg( + "Use the following JSON to accept the proof request from another demo agent." + ) + qr.print_ascii(invert=True) else: raise Exception(f"Error invalid AIP level: {faber_agent.aip}") diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 0b06252c07..d232ca8ad0 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -8,6 +8,7 @@ import subprocess import sys from timeit import default_timer +import base64 from aiohttp import ( web, @@ -599,7 +600,16 @@ async def listen_webhooks(self, webhook_port): f"http://{self.external_host}:{str(webhook_port)}/webhooks" ) app = web.Application() - app.add_routes([web.post("/webhooks/topic/{topic}/", self._receive_webhook)]) + app.add_routes( + [ + web.post("/webhooks/topic/{topic}/", self._receive_webhook), + # route for fetching proof request for connectionless requests + web.get( + "/webhooks/pres_req/{pres_req_id}/", + self._send_connectionless_proof_req, + ), + ] + ) runner = web.AppRunner(app) await runner.setup() self.webhook_site = web.TCPSite(runner, "0.0.0.0", webhook_port) @@ -611,6 +621,36 @@ async def _receive_webhook(self, request: ClientRequest): await self.handle_webhook(topic, payload, request.headers) return web.Response(status=200) + async def service_decorator(self): + # add a service decorator + did_url = "/wallet/did/public" + agent_public_did = await self.admin_GET(did_url) + endpoint_url = ( + "/wallet/get-did-endpoint" + "?did=" + agent_public_did["result"]["did"] + ) + agent_endpoint = await self.admin_GET(endpoint_url) + decorator = { + "recipientKeys": [agent_public_did["result"]["verkey"]], + # "routingKeys": [agent_public_did["result"]["verkey"]], + "serviceEndpoint": agent_endpoint["endpoint"], + } + return decorator + + async def _send_connectionless_proof_req(self, request: ClientRequest): + pres_req_id = request.match_info["pres_req_id"] + url = "/present-proof/records/" + pres_req_id + proof_exch = await self.admin_GET(url) + if not proof_exch: + return web.Response(status=404) + proof_reg_txn = proof_exch["presentation_request_dict"] + proof_reg_txn["~service"] = await self.service_decorator() + objJsonStr = json.dumps(proof_reg_txn) + objJsonB64 = base64.b64encode(objJsonStr.encode("ascii")) + service_url = self.webhook_url + redirect_url = service_url + "/?m=" + objJsonB64.decode("ascii") + print("Redirecting to:", redirect_url) + raise web.HTTPFound(redirect_url) + async def handle_webhook(self, topic: str, payload, headers: dict): if topic != "webhook": # would recurse handler = f"handle_{topic}" From ebfb22c4901d78016470765347a202be3233d281 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Fri, 3 Sep 2021 16:01:05 -0700 Subject: [PATCH 3/9] Display url's Signed-off-by: Ian Costanzo --- demo/runners/faber.py | 4 ++-- demo/runners/support/agent.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/runners/faber.py b/demo/runners/faber.py index 887735fac5..fd2c40e975 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -570,7 +570,7 @@ async def main(args): url = ( faber_agent.agent.webhook_url + "/pres_req/" + pres_req_id + "/" ) - # TODO handle connectionless PR + log_msg(f"Proof request url: {url}") qr = QRCode(border=1) qr.add_data(url) log_msg( @@ -611,7 +611,7 @@ async def main(args): url = ( faber_agent.agent.webhook_url + "/pres_req/" + pres_req_id + "/" ) - # TODO handle connectionless PR + log_msg(f"Proof request url: {url}") qr = QRCode(border=1) qr.add_data(url) log_msg( diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index d232ca8ad0..8e000a7217 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -648,7 +648,7 @@ async def _send_connectionless_proof_req(self, request: ClientRequest): objJsonB64 = base64.b64encode(objJsonStr.encode("ascii")) service_url = self.webhook_url redirect_url = service_url + "/?m=" + objJsonB64.decode("ascii") - print("Redirecting to:", redirect_url) + log_msg(f"Redirecting to: {redirect_url}") raise web.HTTPFound(redirect_url) async def handle_webhook(self, topic: str, payload, headers: dict): From cf70c691171fc0bfdc1ddc5720d12ab379f2b6f4 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 7 Sep 2021 08:46:50 -0700 Subject: [PATCH 4/9] Fix url for qr code Signed-off-by: Ian Costanzo --- demo/runners/faber.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/demo/runners/faber.py b/demo/runners/faber.py index fd2c40e975..06b4fa8782 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -568,7 +568,13 @@ async def main(args): ) pres_req_id = proof_request["presentation_exchange_id"] url = ( - faber_agent.agent.webhook_url + "/pres_req/" + pres_req_id + "/" + "https://" + + os.getenv("DOCKERHOST") + + ":" + + str(faber_agent.agent.admin_port + 1) + + "/webhooks/pres_req/" + + pres_req_id + + "/" ) log_msg(f"Proof request url: {url}") qr = QRCode(border=1) From aadcec9d9ba72a638d70782d6b7e74846aac3529 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 7 Sep 2021 09:10:42 -0700 Subject: [PATCH 5/9] Fix url for qr code Signed-off-by: Ian Costanzo --- demo/runners/faber.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/demo/runners/faber.py b/demo/runners/faber.py index 06b4fa8782..5dde074212 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -568,10 +568,10 @@ async def main(args): ) pres_req_id = proof_request["presentation_exchange_id"] url = ( - "https://" - + os.getenv("DOCKERHOST") - + ":" - + str(faber_agent.agent.admin_port + 1) + "http://" + + os.getenv("DOCKERHOST").replace( + "{PORT}", str(faber_agent.agent.admin_port + 1) + ) + "/webhooks/pres_req/" + pres_req_id + "/" From 58bd7a5d15e829442c70d6d46e4ae170ef09b07c Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 7 Sep 2021 09:38:49 -0700 Subject: [PATCH 6/9] Fix url's for proof request Signed-off-by: Ian Costanzo --- demo/runners/faber.py | 13 ++++++---- docs/assets/inbound-messaging.png | Bin 0 -> 55628 bytes docs/assets/inbound-messaging.puml | 37 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 docs/assets/inbound-messaging.png create mode 100644 docs/assets/inbound-messaging.puml diff --git a/demo/runners/faber.py b/demo/runners/faber.py index 5dde074212..4f127cded8 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -142,7 +142,6 @@ def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracin "issuanceDate": "2020-01-01T12:00:00Z", "credentialSubject": { "type": ["PermanentResident"], - # "id": "", "givenName": "ALICE", "familyName": "SMITH", "gender": "Female", @@ -580,7 +579,7 @@ async def main(args): qr = QRCode(border=1) qr.add_data(url) log_msg( - "Use the following JSON to accept the proof request from another demo agent." + "Scan the following QR code to accept the proof request from a mobile agent." ) qr.print_ascii(invert=True) @@ -615,13 +614,19 @@ async def main(args): ) pres_req_id = proof_request["pres_ex_id"] url = ( - faber_agent.agent.webhook_url + "/pres_req/" + pres_req_id + "/" + "http://" + + os.getenv("DOCKERHOST").replace( + "{PORT}", str(faber_agent.agent.admin_port + 1) + ) + + "/webhooks/pres_req/" + + pres_req_id + + "/" ) log_msg(f"Proof request url: {url}") qr = QRCode(border=1) qr.add_data(url) log_msg( - "Use the following JSON to accept the proof request from another demo agent." + "Scan the following QR code to accept the proof request from a mobile agent." ) qr.print_ascii(invert=True) else: diff --git a/docs/assets/inbound-messaging.png b/docs/assets/inbound-messaging.png new file mode 100644 index 0000000000000000000000000000000000000000..804c291e86900a30d82b03f5954cb1f2a5b76394 GIT binary patch literal 55628 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 ZE5tviV8p^1DFPB%M9) zu(&Lfe!C=z)ejP^d{tOHSQCU^rO`qaKJ|=tmcNfTrv*5jpT3RdFo^~ySSu{3#?FSe z^hB$|-M2khXK2#jh*#IUJpXdp0OWTG@ls|*~6z3EwXzeo@JZsUUC!yn|(3)1@q zbO}5rLegc5vsrkq38nzO|DBnjD6^ z>y|BNs)dvk6(K_;q2tcOhf@$FbvuwJZvKdQ`P9@DbVR9-%*@PyV0iuwLSdSdbt1yU zqbXxmbCv*uSRuh(3G)H$vPBeqgy6GDPD*-xiaR?9qqL{uL%MN7`#0R~3iD8I0 z+G#+Nt^*gudhxwP!3>$1nF)~&_9g0ackX1|ucr;Hj7Ta46&z+i4My;hE8a870oG$* zz_O^Ju70gI5CROcCknBP+X0{qaWlAEt{^ODXYhCyt{KKSfAI4wVxV24Bw7Z>#^i+V zeAaMOt*orHpKI2U9g`Y{xTzNi8cyIKaA})|`tO6;Rq_{u#kUjH9LJAh_|Pr17(s6+ zurb1dE-X3~16h#C;PFfWuBe8FhQ59bOb`x8KqvW`e`Z;!rt-eAaijU{m-Cc!ky%hK zWypTLhD?YncMG3$bXfF&Idv(>#N|9#NPhhIvC>-4^jcX-iEL3i(kH?!1Mc-GrES>f z&yozdI5_(lJ94PW4xho?_8sJ!EA@rZx+rN{L3Clx0z%@&$#RHtLemS<;i6!9kvelP zWqG(=w2=JkV~#>*@X07iNI;y-hxhO2(Ni85oRr|S1mG#6tun1{l23qvCH%e(OkQ}X zRsUeomSjK9_lFtA%0AOZk_|=pj5kkIU30Tg?h}Q{K=Ci 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 literal 0 HcmV?d00001 diff --git a/docs/assets/inbound-messaging.puml b/docs/assets/inbound-messaging.puml new file mode 100644 index 0000000000..4607463b28 --- /dev/null +++ b/docs/assets/inbound-messaging.puml @@ -0,0 +1,37 @@ +@startuml + +participant "Inbound\nMessage\nHandler" as oag +participant "http\nTransport" as ht +participant "Internal\nTransport\nManager" as itm +participant "Inbound\nSession" as is +participant "Conductor" as con +participant "Dispatcher" as disp +participant "Responder" as resp +participant "Message\nProtocol\nHandler" as mh + + +oag -> ht: "inbound_message_handler()" +ht->itm: "create_session()" +itm -> is: "create" +is --> itm +itm --> ht +ht --> is: "receive()" +is --> is: "parse_inbound()" +is --> is: "receive_inbound()" +is --> is: "process_inbound()" +is --> is: "inbound_handler()" +is --> con: "inbound_message_router()" +con --> disp: "queue_message()" +disp --> disp: "handle_message()" +disp --> disp: "make_message()" +disp --> resp: "create()" +disp --> mh: "handle()" +mh-->resp: "send_reply()" +mh --> disp: "" +disp --> con: "" +con --> con: "dispatch_complete()" +con --> is +is --> ht + + +@enduml From 6b3d8710e1b2ed19066872dd9904661dcf543826 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 7 Sep 2021 11:26:58 -0700 Subject: [PATCH 7/9] Use birth date instead of age in cred and proof request Signed-off-by: Ian Costanzo --- demo/runners/faber.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/demo/runners/faber.py b/demo/runners/faber.py index 4f127cded8..39a1ae5e1d 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -4,6 +4,7 @@ import os import sys import time +import datetime from aiohttp import ClientError from qrcode import QRCode @@ -69,13 +70,17 @@ def connection_ready(self): return self._connection_ready.done() and self._connection_ready.result() def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracing): + age = 24 + d = datetime.date.today() + birth_date = datetime.date(d.year - age, d.month, d.day) + birth_date_format = "%Y%m%d" if aip == 10: # define attributes to send for credential self.cred_attrs[cred_def_id] = { "name": "Alice Smith", "date": "2018-05-28", "degree": "Maths", - "age": "24", + "birthdate_dateint": birth_date.strftime(birth_date_format), "timestamp": str(int(time.time())), } @@ -102,7 +107,7 @@ def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracin "name": "Alice Smith", "date": "2018-05-28", "degree": "Maths", - "age": "24", + "birthdate_dateint": birth_date.strftime(birth_date_format), "timestamp": str(int(time.time())), } @@ -164,6 +169,10 @@ def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracin def generate_proof_request_web_request( self, aip, cred_type, revocation, exchange_tracing, connectionless=False ): + age = 18 + d = datetime.date.today() + birth_date = datetime.date(d.year - age, d.month, d.day) + birth_date_format = "%Y%m%d" if aip == 10: req_attrs = [ { @@ -198,9 +207,9 @@ def generate_proof_request_web_request( req_preds = [ # test zero-knowledge proofs { - "name": "age", - "p_type": ">=", - "p_value": 18, + "name": "birthdate_dateint", + "p_type": "<=", + "p_value": int(birth_date.strftime(birth_date_format)), "restrictions": [{"schema_name": "degree schema"}], } ] @@ -261,9 +270,9 @@ def generate_proof_request_web_request( req_preds = [ # test zero-knowledge proofs { - "name": "age", - "p_type": ">=", - "p_value": 18, + "name": "birthdate_dateint", + "p_type": "<=", + "p_value": int(birth_date.strftime(birth_date_format)), "restrictions": [{"schema_name": "degree schema"}], } ] @@ -387,7 +396,13 @@ async def main(args): if faber_agent.cred_type == CRED_FORMAT_INDY: faber_agent.public_did = True faber_schema_name = "degree schema" - faber_schema_attrs = ["name", "date", "degree", "age", "timestamp"] + faber_schema_attrs = [ + "name", + "date", + "degree", + "birthdate_dateint", + "timestamp", + ] await faber_agent.initialize( the_agent=agent, schema_name=faber_schema_name, From 6a36213243a51772d6b8414fa036d2db2755ee74 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 7 Sep 2021 12:25:49 -0700 Subject: [PATCH 8/9] Update some docs Signed-off-by: Ian Costanzo --- demo/AliceGetsAPhone.md | 13 +++++++++++++ demo/README.md | 2 ++ 2 files changed, 15 insertions(+) diff --git a/demo/AliceGetsAPhone.md b/demo/AliceGetsAPhone.md index 1f4fa15108..4f0b63d710 100644 --- a/demo/AliceGetsAPhone.md +++ b/demo/AliceGetsAPhone.md @@ -19,6 +19,7 @@ This demo also introduces revocation of credentials. - [Present the Proof](#present-the-proof) - [Review the Proof](#review-the-proof) - [Revoke the Credential and Send Another Proof Request](#revoke-the-credential-and-send-another-proof-request) +- [Send a Connectionless Proof Request](#send-a-connectionless-proof-request) - [Conclusion](#conclusion) ## Getting Started @@ -301,6 +302,18 @@ Once that is done, try sending another proof request and see what happens! Exper Revocation +## Send a Connectionless Proof Request + +A connectionless proof request works the same way as a regular proof request, however it does not require a connection to be established between the Verifier and Holder/Prover. + +This is supported in the Faber demo, however note that it will only work when running Faber on the Docker playground service [Play with Docker](https://labs.play-with-docker.com/) (or on [Play with VON](http://play-with-von.vonx.io)). (This is because both the Faber agent *and* controller both need to be exposed to the mobile agent.) + +If you have gone through the above steps, you can delete the Faber connection in your mobile agent (however *do not* delete the credential that Faber issued to you). + +Then in the faber demo, select option `2a` - Faber will display a QR code which you can scan with your mobile agent. You will see the same proof request displayed in your mobile agent, which you can respond to. + +Behind the scenes, the Faber controller delivers the proof request information (linked from the url encoded in the QR code) directly to your mobile agent, without establishing and agent-to-agent connection first. If you are interested in the underlying mechanics, you can review the `faber.py` code in the repository. + ## Conclusion That’s the Faber-Mobile Alice demo. Feel free to play with the Swagger API and experiment further and figure out what an instance of a controller has to do to make things work. diff --git a/demo/README.md b/demo/README.md index a561633cdb..ae8a32ad69 100644 --- a/demo/README.md +++ b/demo/README.md @@ -176,6 +176,8 @@ When ready to test the credentials exchange protocols, go to the Faber prompt, e You don't need to do anything with Alice's agent - her agent is implemented to automatically receive credentials and respond to proof requests. +Note there is an option "2a" to initiate a connectionless proof - you can execute this option but it woll only work end-to-end when [connecting to Faber from a mobile agent](AliceGetsAPhone.md). + ## Additional Options in the Alice/Faber demo You can enable support for various aca-py features by providing additional command-line arguements when starting up `alice` or `faber`. From ced0ce9f68faa14389ef3081f8b5eec1625d88da Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 7 Sep 2021 12:27:35 -0700 Subject: [PATCH 9/9] Update some docs Signed-off-by: Ian Costanzo --- demo/AriesOpenAPIDemo.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/AriesOpenAPIDemo.md b/demo/AriesOpenAPIDemo.md index f42a1597c1..8edfb71dff 100644 --- a/demo/AriesOpenAPIDemo.md +++ b/demo/AriesOpenAPIDemo.md @@ -502,8 +502,8 @@ Finally, we need put into the JSON the data values for the `credential_preview` "value": "Maths" }, { - "name": "age", - "value": "24" + "name": "birthdate_dateint", + "value": "19640101" } ``` @@ -640,9 +640,9 @@ From the Faber browser tab, get ready to execute the **`POST /present-proof/send }, "requested_predicates": { "0_age_GE_uuid": { - "name": "age", - "p_type": ">=", - "p_value": 18, + "name": "birthdate_dateint", + "p_type": "<=", + "p_value": 20030101, "restrictions": [ { "cred_def_id": "SsX9siFWXJyCAmXnHY514N:3:CL:8:faber.agent.degree_schema"