From 44aac4624d71b16f81aaacbac5810cb5136d8d53 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Wed, 29 May 2024 09:12:13 -0700 Subject: [PATCH 1/2] Add V20 cred exchange support Signed-off-by: Ian Costanzo --- docker/docker-compose.yml | 1 + docker/manage | 1 + issuer_controller/src/app.py | 40 +++++ issuer_controller/src/issuer.py | 153 +++++++++++++++++- .../von_pipeline/credssubmitter.py | 27 ++-- 5 files changed, 200 insertions(+), 22 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e5122f0..10ec325 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -160,6 +160,7 @@ services: environment: - WEBHOOK_PORT=${WEBHOOK_PORT} - VONX_API_URL=${VONX_API_URL:-http://myorg-controller:8000} + - ISSUE_CRED_VERSION=${ISSUE_CRED_VERSION:-V10} # [pipeline data source (directory)] #- EAO_MDB_DB_HOST=${EAO_MDB_DB_HOST:-mongo} #- EAO_MDB_DB_PORT=${EAO_MDB_DB_PORT:-27017} diff --git a/docker/manage b/docker/manage index 0109d60..33a49f6 100755 --- a/docker/manage +++ b/docker/manage @@ -145,6 +145,7 @@ configureEnvironment () { export APPLICATION_URL=${APPLICATION_URL-http://localhost:${WEB_HTTP_PORT:-5000}} export ENDPOINT_URL=http://${ENDPOINT_HOST-$DOCKERHOST:${WEB_HTTP_PORT:-5001}} export VONX_API_URL=http://myorg-controller:${WEBHOOK_PORT} + export ISSUE_CRED_VERSION=${ISSUE_CRED_VERSION:-"V10"} # myorg-controller # specify this as anything other than "true" to force manual connection to the TOB agent diff --git a/issuer_controller/src/app.py b/issuer_controller/src/app.py index a669330..9c25ea7 100644 --- a/issuer_controller/src/app.py +++ b/issuer_controller/src/app.py @@ -106,6 +106,32 @@ def submit_credential(): return response +@app.route('/issue-credential-v20', methods=['POST']) +def submit_credential_v20(): + """ + Exposed method to proxy credential issuance requests. + """ + if not issuer.tob_connection_synced(): + abort(503, "Connection not yet synced") + + start_time = time.perf_counter() + method = 'submit_credential_v20.batch' + + if not request.json: + end_time = time.perf_counter() + issuer.log_timing_method(method, start_time, end_time, False) + abort(400) + + cred_input = request.json + + response = issuer.handle_send_credential_v20(cred_input) + + end_time = time.perf_counter() + issuer.log_timing_method(method, start_time, end_time, True) + + return response + + @app.route("/api/agentcb/topic//", methods=["POST"]) @authentication.api_key_required def agent_callback(topic): @@ -141,6 +167,13 @@ def agent_callback(topic): else: response = jsonify({}) + elif topic == issuer.TOPIC_CREDENTIALS_V20 or topic == issuer.TOPIC_CREDENTIALS_V20_INDY: + if "state" in message: + method = method + '.' + message["state"] + response = issuer.handle_credentials_v20(message["state"], message) + else: + response = jsonify({}) + elif topic == issuer.TOPIC_PRESENTATIONS: if "state" in message: method = method + "." + message["state"] @@ -148,6 +181,13 @@ def agent_callback(topic): else: response = jsonify({}) + elif topic == issuer.TOPIC_PRESENTATIONS_V20: + if "state" in message: + method = method + '.' + message["state"] + response = issuer.handle_presentations_v20(message["state"], message) + else: + response = jsonify({}) + elif topic == issuer.TOPIC_GET_ACTIVE_MENU: response = issuer.handle_get_active_menu(message) diff --git a/issuer_controller/src/issuer.py b/issuer_controller/src/issuer.py index 2d562e6..24050b3 100644 --- a/issuer_controller/src/issuer.py +++ b/issuer_controller/src/issuer.py @@ -659,6 +659,10 @@ def get_credential_response(cred_exch_id): TOPIC_CONNECTIONS = "connections" TOPIC_CONNECTIONS_ACTIVITY = "connections_actvity" TOPIC_CREDENTIALS = "issue_credential" +TOPIC_CREDENTIALS_V20 = "issue_credential_v2_0" +TOPIC_CREDENTIALS_V20_INDY = "issue_credential_v2_0_indy" +TOPIC_PRESENTATIONS = "handle_present_proof" +TOPIC_PRESENTATIONS_V20 = "present_proof_v2_0" TOPIC_PRESENTATIONS = "presentations" TOPIC_GET_ACTIVE_MENU = "get-active-menu" TOPIC_PERFORM_MENU_ACTION = "perform-menu-action" @@ -694,7 +698,7 @@ def handle_credentials(state, message): ) else: pass - if state == "credential_acked": + if state == "credential_acked" or state == "done": # raise 10% errors do_error = random.randint(1, 100) if do_error <= ACK_ERROR_PCT: @@ -711,11 +715,42 @@ def handle_credentials(state, message): return jsonify({"message": state}) +def handle_credentials_v20(state, message): + start_time = time.perf_counter() + method = "Handle callback:" + state + log_timing_event(method, message, start_time, None, False) + + if "thread_id" in message: + set_credential_thread_id( + message["cred_ex_id"], message["thread_id"] + ) + else: + pass + if state == "credential_acked" or state == "done": + # raise 10% errors + #do_error = random.randint(1, 100) + #if do_error <= 10: + # raise Exception("Fake exception to test error handling: " + message["thread_id"]) + response = {"success": True, "result": message["cred_ex_id"]} + add_credential_response(message["cred_ex_id"], response) + + end_time = time.perf_counter() + processing_time = end_time - start_time + log_timing_event(method, message, start_time, end_time, True, outcome=str(state)) + + return jsonify({"message": "state"}) + + def handle_presentations(state, message): # TODO auto-respond to proof requests return jsonify({"message": state}) +def handle_presentations_v20(state, message): + # TODO auto-respond to proof requests + return jsonify({"message": state}) + + def handle_get_active_menu(message): # TODO add/update issuer info? return jsonify({}) @@ -763,10 +798,17 @@ def run(self): ) response.raise_for_status() cred_data = response.json() + credential_exchange_id = None if "credential_exchange_id" in cred_data: + credential_exchange_id = cred_data["credential_exchange_id"] result_available = add_credential_request( cred_data["credential_exchange_id"] ) + elif "cred_ex_id" in cred_data: + credential_exchange_id = cred_data["cred_ex_id"] + result_available = add_credential_request( + cred_data["cred_ex_id"] + ) else: raise Exception(json.dumps(cred_data)) @@ -775,12 +817,12 @@ def run(self): MAX_CRED_RESPONSE_TIMEOUT ): add_credential_timeout_report( - cred_data["credential_exchange_id"], cred_data["thread_id"] + credential_exchange_id, cred_data["thread_id"] ) LOGGER.error( "Got credential TIMEOUT: %s %s %s", cred_data["thread_id"], - cred_data["credential_exchange_id"], + credential_exchange_id, cred_data["connection_id"], ) end_time = time.perf_counter() @@ -791,7 +833,7 @@ def run(self): False, data={ "thread_id": cred_data["thread_id"], - "credential_exchange_id": cred_data["credential_exchange_id"], + "credential_exchange_id": credential_exchange_id, "Error": "Timeout", "elapsed_time": (end_time - start_time), }, @@ -807,7 +849,7 @@ def run(self): # there should be some form of response available self.cred_response = get_credential_response( - cred_data["credential_exchange_id"] + credential_exchange_id ) except Exception as exc: @@ -818,11 +860,11 @@ def run(self): outcome = str(exc) if cred_data: add_credential_exception_report( - cred_data["credential_exchange_id"], exc + credential_exchange_id, exc ) data = { "thread_id": cred_data["thread_id"], - "credential_exchange_id": cred_data["credential_exchange_id"], + "credential_exchange_id": credential_exchange_id, "Error": str(exc), "elapsed_time": (end_time - start_time), } @@ -935,3 +977,100 @@ def handle_send_credential(cred_input): print(" ", processing_time / processed_count, "seconds per credential") return jsonify(cred_responses) + + +def handle_send_credential_v20(cred_input): + """ + # other sample data + sample_credentials = [ + { + "schema": "ian-registration.ian-ville", + "version": "1.0.0", + "attributes": { + "corp_num": "ABC12345", + "registration_date": "2018-01-01", + "entity_name": "Ima Permit", + "entity_name_effective": "2018-01-01", + "entity_status": "ACT", + "entity_status_effective": "2019-01-01", + "entity_type": "ABC", + "registered_jurisdiction": "BC", + "effective_date": "2019-01-01", + "expiry_date": "" + } + }, + { + "schema": "ian-permit.ian-ville", + "version": "1.0.0", + "attributes": { + "permit_id": str(uuid.uuid4()), + "entity_name": "Ima Permit", + "corp_num": "ABC12345", + "permit_issued_date": "2018-01-01", + "permit_type": "ABC", + "permit_status": "OK", + "effective_date": "2019-01-01" + } + } + ] + """ + # construct and send the credential + global app_config + + agent_admin_url = app_config["AGENT_ADMIN_URL"] + + start_time = time.perf_counter() + processing_time = 0 + processed_count = 0 + + # let's send a credential! + cred_responses = [] + for credential in cred_input: + cred_def_key = "CRED_DEF_" + credential["schema"] + "_" + credential["version"] + credential_definition_id = app_config["schemas"][cred_def_key] + + credential_attributes = [] + for attribute in credential["attributes"]: + credential_attributes.append({ + "name": attribute, + "value": credential["attributes"][attribute] + }) + cred_offer = { + "credential_preview": { + "@type": "issue-credential/2.0/credential-preview", + "attributes": credential_attributes + }, + "filter": { + "indy": { + "schema_id": app_config["schemas"][ + "SCHEMA_" + credential["schema"] + "_" + credential["version"] + ], + "schema_name": credential["schema"], + "issuer_did": app_config["DID"], + "schema_version": credential["version"], + "schema_issuer_did": app_config["DID"], + "cred_def_id": credential_definition_id, + } + }, + "comment": "", + "connection_id": app_config["TOB_CONNECTION"], + } + do_trace = random.randint(1, 100) + if do_trace <= TRACE_MSG_PCT: + cred_offer["trace"] = True + thread = SendCredentialThread( + credential_definition_id, + cred_offer, + agent_admin_url + "/issue-credential-2.0/send", + ADMIN_REQUEST_HEADERS, + ) + thread.start() + thread.join() + cred_responses.append(thread.cred_response) + processed_count = processed_count + 1 + + processing_time = time.perf_counter() - start_time + print(">>> Processed", processed_count, "credentials in", processing_time) + print(" ", processing_time/processed_count, "seconds per credential") + + return jsonify(cred_responses) diff --git a/issuer_pipeline/von_pipeline/credssubmitter.py b/issuer_pipeline/von_pipeline/credssubmitter.py index a962480..2e58f08 100644 --- a/issuer_pipeline/von_pipeline/credssubmitter.py +++ b/issuer_pipeline/von_pipeline/credssubmitter.py @@ -30,6 +30,10 @@ AGENT_URL = os.environ.get('VONX_API_URL', 'http://localhost:5000') +ISSUE_CRED_VERSION = os.getenv('ISSUE_CRED_VERSION', 'V10') +if not ISSUE_CRED_VERSION in ['V10', 'V20']: + raise Exception(f"Unsupported Issue Credential version: {ISSUE_CRED_VERSION}") + CREDS_BATCH_SIZE = 3000 CREDS_REQUEST_SIZE = 5 # use 1 because it's more likely to trigger deadlocks MAX_CREDS_REQUESTS = 16 @@ -48,25 +52,22 @@ async def submit_cred_batch(http_client, creds): 'Credentials could not be processed: {}'.format(await response.text()) ) result_json = await response.json() - #print('Response from von-x:\n{}\n'.format(result_json)) return result_json except Exception as exc: print(exc) raise -async def submit_cred(http_client, attrs, schema, version): +async def submit_cred_batch_v20(http_client, creds): try: response = await http_client.post( - '{}/issue-credential'.format(AGENT_URL), - params={'schema': schema, 'version': version}, - json=attrs + '{}/issue-credential-v20'.format(AGENT_URL), + json=creds ) if response.status != 200: raise RuntimeError( - 'Credential could not be processed: {}'.format(await response.text()) + 'Credentials could not be processed: {}'.format(await response.text()) ) result_json = await response.json() - #print('Response from von-x:\n{}\n'.format(result_json)) return result_json except Exception as exc: print(exc) @@ -93,14 +94,10 @@ async def post_credentials(http_client, conn, credentials): cur2 = None results = None try: - #print('=============') - #print(post_creds) - #print('=============') - # old code for submitting one credential at a time - # result_json = await submit_cred(http_client, credential['CREDENTIAL_JSON'], credential['SCHEMA_NAME'], credential['SCHEMA_VERSION']) - results = await submit_cred_batch(http_client, post_creds) - - #print("Posted = ", len(credentials), ", results = ", len(results)) + if ISSUE_CRED_VERSION == "V20": + results = await submit_cred_batch_v20(http_client,post_creds) + else: + results = await submit_cred_batch(http_client, post_creds) for i in range(len(credentials)): credential = credentials[i] From 90555c8b734192f07541866c38291088eed6308f Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Wed, 29 May 2024 09:19:41 -0700 Subject: [PATCH 2/2] Make V20 cred exchange the default Signed-off-by: Ian Costanzo --- docker/docker-compose.yml | 2 +- docker/manage | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 10ec325..62d2cfc 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -160,7 +160,7 @@ services: environment: - WEBHOOK_PORT=${WEBHOOK_PORT} - VONX_API_URL=${VONX_API_URL:-http://myorg-controller:8000} - - ISSUE_CRED_VERSION=${ISSUE_CRED_VERSION:-V10} + - ISSUE_CRED_VERSION=${ISSUE_CRED_VERSION:-V20} # [pipeline data source (directory)] #- EAO_MDB_DB_HOST=${EAO_MDB_DB_HOST:-mongo} #- EAO_MDB_DB_PORT=${EAO_MDB_DB_PORT:-27017} diff --git a/docker/manage b/docker/manage index 33a49f6..864466d 100755 --- a/docker/manage +++ b/docker/manage @@ -145,7 +145,7 @@ configureEnvironment () { export APPLICATION_URL=${APPLICATION_URL-http://localhost:${WEB_HTTP_PORT:-5000}} export ENDPOINT_URL=http://${ENDPOINT_HOST-$DOCKERHOST:${WEB_HTTP_PORT:-5001}} export VONX_API_URL=http://myorg-controller:${WEBHOOK_PORT} - export ISSUE_CRED_VERSION=${ISSUE_CRED_VERSION:-"V10"} + export ISSUE_CRED_VERSION=${ISSUE_CRED_VERSION:-"V20"} # myorg-controller # specify this as anything other than "true" to force manual connection to the TOB agent