diff --git a/.gitignore b/.gitignore index 95c00b7..5953e05 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ dist *.egg-info *.ipynb_checkpoints *.pyc +quick_*.sh +settings.sh +hfmodels-contract/ \ No newline at end of file diff --git a/medperf-contract/CMakeLists.txt b/medperf-contract/CMakeLists.txt new file mode 100644 index 0000000..d4f1b51 --- /dev/null +++ b/medperf-contract/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This is necessary to get at the definitions necessary +# for the std::string class +INCLUDE(exchange_common) +LIST(APPEND WASM_LIBRARIES ${EXCHANGE_LIB}) +LIST(APPEND WASM_INCLUDES ${EXCHANGE_INCLUDES}) + + +INCLUDE(medperf_common.cmake) +LIST(APPEND WASM_LIBRARIES ${MEDPERF_LIB}) +LIST(APPEND WASM_INCLUDES ${MEDPERF_INCLUDES}) + +ADD_LIBRARY(${MEDPERF_LIB} STATIC ${MEDPERF_SOURCES}) +TARGET_INCLUDE_DIRECTORIES(${MEDPERF_LIB} PUBLIC ${MEDPERF_INCLUDES}) + +SET_PROPERTY(TARGET ${MEDPERF_LIB} APPEND_STRING PROPERTY COMPILE_OPTIONS "${WASM_BUILD_OPTIONS}") +SET_PROPERTY(TARGET ${MEDPERF_LIB} APPEND_STRING PROPERTY LINK_OPTIONS "${WASM_LINK_OPTIONS}") +SET_TARGET_PROPERTIES(${MEDPERF_LIB} PROPERTIES EXCLUDE_FROM_ALL TRUE) + +BUILD_CONTRACT(medperf_token_object contracts/token_object.cpp) + +# ----------------------------------------------------------------- +INCLUDE(Python) +BUILD_WHEEL(medperf medperf_token_object) \ No newline at end of file diff --git a/medperf-contract/MANIFEST b/medperf-contract/MANIFEST new file mode 100644 index 0000000..7b26b13 --- /dev/null +++ b/medperf-contract/MANIFEST @@ -0,0 +1,32 @@ +MANIFEST.in +./setup.py +./pdo/medperf/wsgi/provision_token_issuer.py +./pdo/medperf/wsgi/__init__.py +./pdo/medperf/wsgi/add_endpoint.py +./pdo/medperf/wsgi/provision_token_object.py +./pdo/medperf/wsgi/info.py +./pdo/medperf/wsgi/process_capability.py +./pdo/medperf/operations/use_dataset.py +./pdo/medperf/operations/__init__.py +./pdo/medperf/plugins/medperf_token_object.py +./pdo/medperf/plugins/__init__.py +./pdo/medperf/plugins/medperf_guardian.py +./pdo/medperf/__init__.py +./pdo/medperf/resources/resources.py +./pdo/medperf/resources/__init__.py +./pdo/medperf/common/guardian_service.py +./pdo/medperf/common/capability_keystore.py +./pdo/medperf/common/secrets.py +./pdo/medperf/common/__init__.py +./pdo/medperf/common/endpoint_registry.py +./pdo/medperf/common/utility.py +./pdo/medperf/common/capability_keys.py +./pdo/medperf/scripts/__init__.py +./pdo/medperf/scripts/guardianCLI.py +./pdo/medperf/scripts/scripts.py +./scripts/gs_stop.sh +./scripts/gs_start.sh +./scripts/gs_status.sh +./context/tokens.toml +./etc/medperf.toml +./etc/guardian_service.toml \ No newline at end of file diff --git a/medperf-contract/MANIFEST.in b/medperf-contract/MANIFEST.in new file mode 100644 index 0000000..7eb8ee6 --- /dev/null +++ b/medperf-contract/MANIFEST.in @@ -0,0 +1,4 @@ +recursive-include ../build/medperf-contract *.b64 +recursive-include etc *.toml +recursive-include context *.toml +recursive-include scripts *.sh diff --git a/medperf-contract/PROTOCOL.md b/medperf-contract/PROTOCOL.md new file mode 100644 index 0000000..fba77fa --- /dev/null +++ b/medperf-contract/PROTOCOL.md @@ -0,0 +1,124 @@ +## Workflow + +```mermaid +sequenceDiagram + participant E as Experiment Committee + participant PDO as PDO Contract (SGX) + participant G as Digital Guardian Service + participant D as Data Owner + participant M as MedPerf Server + %% participant MO as Model Owner + note over G, M: experiment-data association (assume finished) + note over G, M: model-data association (assume finished) + note over M, PDO: assume the states data are synchronized + loop Attestation + PDO -> G: + end + D ->> PDO: create token_issuer and call cmd_mint_token + PDO ->> PDO: generate new identity token_issuer and new contract token.token_object + D ->> PDO: transfer token.token_object to new owner token_experitment1 + PDO ->> PDO: generate new identity token_expertiment1 and new contract token.token_experiment1 + note over E, PDO: owership transferred to Experiment committee + note over M: notification about token/dataset availiability + box Assume Trusted + participant M + end + box SGX Enclave + participant G + end + E ->> PDO: request the inference on specific model model_id, experiment_id (cmd_use_dataset) + activate PDO + PDO ->> PDO: policy evaluation + note right of PDO: policy:
experiment_id = experiment_id'
model_id in {model_id} + PDO ->> E: return capabilities, encoded with urls for dockerfile and weights of the granted model.identity of + deactivate PDO + E ->> G: request access to service with capability (storage ) + E ->> D: + activate G + note over G: pull/download/deploy mlcube (the model) + G ->> G: run inference over the dataset + G ->> D: return result (basic verification with hash) + D ->> E: return results + deactivate G +``` + + +## Protocols + +From the dataset owner side: ++ The dataset is identified by its hash. ++ Tokens are minted based on hash-based binding. ++ Guardian service of the token exposes an api to the callers + + +## Core functions + ++ Allow data owner to tokenize a dataset (into a PDO contract) + + with default policy checking the registration information from MedPerf + + ownership transfer to experiment committee + + generate capability to grant access to the guardian service + ++ Allow experiment committee to initilize the use of data by interacting with PDO + + capabilities are published on MedPerf server (or PDO states/ledger?) for downloading + ++ Allow data owner to host a guardian service, which exposes interface (local) to initilize the test + + data owner downloads the capability from the server (or ledger) + + data owner feeds the capability to guardian service to initialize the experiment + + guardian service publish experiment results to the server (or ledger) + + allows access control? + + + +### Guardian service + +Datasets are hosted behind the service. + + + + +The guardian service is co-located with the dataset. guadian service provides a wsgi api to process the capability. Capability encodes `{model_id, dataset_id, url_to_docker, url_to_weights}`. After receiving capability, the guardian service: +1. pull/build docker images from `url_to_docker` +2. download weights from `url_to_weights` +3. model up and run over `dataset_id` + +no execution integrity for now. + + + + +### Contract methods + +new contract methods under the class `ww::medperf::token_object` + +`initialize`: public methode, mint token for the dataset, actual method behind `cmd_mint_token` + 1. kvs of the experiment/model/dataset info + 2. Store the registered metadata from medperf service (synthetic for PoC). + 3. set a `max_evaluation` + +| Keys | Values | +| ---- | ---- | +| experiment_id | identifier for the experimetn | +| model_id | identifier for the model | +| {urls} | url to model assets | +| dataset_id | hash of the dataset | +| max_evaluation | most models that allowed to evaluate | +| cur_evaluation | 0 | +| approved_capability| {} | +| TBA | + +`get_datasetinfo`: public method, get the non-secret kvs info of dataset token +return the information associated with the dataset. + +capability -links to- identity // invoked by the dataowner only + + + +`use_dataset(model_id, dataset_id, experiment_id)`: only invoked by TO + 1. check if model_id, dataset_id, experiment_id are in the kv storage + 2. cur_evaluation + 1, if cur_evaluation = max_evaluation, fail + 3. invoke `get_capability` and return secretly encoded {urls}. + +allows multiple models in one call + +`get_capability`: only invoked by TO, parse capability kv \ No newline at end of file diff --git a/medperf-contract/README.md b/medperf-contract/README.md new file mode 100644 index 0000000..ae03f51 --- /dev/null +++ b/medperf-contract/README.md @@ -0,0 +1,40 @@ + + +# PDO-enhanced MedPerf Workflow +This document includes an enhanced workflow for Federated Evaluation (FE) in machine learning, aiming in providing a policy-enforced digital asset usage framwork with tokenization. The framework is built upon the FE workflow from [MedPerf](https://github.com/mlcommons/medperf). + +## Note + +The initial version of this project only covers the protection of dataset. Other digital assets, including the model (weights)and experiment results are not included for now. + + +## Test with the MedPerf tutorial +The existing test simulates the pdo-enhanced workflow by running both components simultaneously. The pdo command hacks into the medperf database directly to simulate the information updates. + +Set up PDO test environment and have the [MedPerf](https://github.com/mlcommons/medperf) installed and built. Then set up environment variables `MEDPERF_SQLITE_PATH`, `MEDPERF_VENV_PATH` and `MEDPERF_HOME`. + +Simply run [easy_test.sh](./easy_test.sh) to start the test. + + + + + + + +## Testing workflow ++ create token issuer and mint the token for the dataset. ++ token issuer transfers token to model_owner1 ++ model_owner1 invoke `cmd_use_dataset` with specific `model_id` and `dataset_id` + + get capability for one model, increase the count + + call service to run inference ++ model_owner1 invoke `cmd_use_dataset` with specific `model_id` and `dataset_id` + + max_evaluation exceeds, fail + +For detailed description of the whole protocol, check [here](./PROTOCOL.md). \ No newline at end of file diff --git a/medperf-contract/context/tokens.toml b/medperf-contract/context/tokens.toml new file mode 100644 index 0000000..72e9379 --- /dev/null +++ b/medperf-contract/context/tokens.toml @@ -0,0 +1,54 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------- +# token ${token} +# ----------------------------------------------------------------- +[token.${token}.asset_type] +module = "pdo.exchange.plugins.asset_type" +identity = "token_type" +source = "${ContractFamily.Exchange.asset_type.source}" +name = "${token}" +description = "asset type for ${token} token objects" +link = "http://" + +[token.${token}.vetting] +module = "pdo.exchange.plugins.vetting" +identity = "token_vetting" +source = "${ContractFamily.Exchange.vetting.source}" +asset_type_context = "@{..asset_type}" + +[token.${token}.guardian] +module = "pdo.medperf.plugins.medperf_guardian" +url = "${url}" +identity = "${..token_issuer.identity}" +token_issuer_context = "@{..token_issuer}" +service_only = true + +[token.${token}.token_issuer] +module = "pdo.exchange.plugins.token_issuer" +identity = "token_issuer" +source = "${ContractFamily.Exchange.token_issuer.source}" +token_object_context = "@{..token_object}" +vetting_context = "@{..vetting}" +guardian_context = "@{..guardian}" +description = "issuer for token ${token}" +count = 1 + +[token.${token}.token_object] +module = "pdo.medperf.plugins.medperf_token_object" +identity = "${..token_issuer.identity}" +source = "${ContractFamily.medperf.token_object.source}" +token_issuer_context = "@{..token_issuer}" +data_guardian_context = "@{..guardian}" diff --git a/medperf-contract/contracts/token_object.cpp b/medperf-contract/contracts/token_object.cpp new file mode 100644 index 0000000..0ffa95f --- /dev/null +++ b/medperf-contract/contracts/token_object.cpp @@ -0,0 +1,80 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "Dispatch.h" + +#include "Cryptography.h" +#include "KeyValue.h" +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Types.h" +#include "Util.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "contract/base.h" +#include "contract/attestation.h" +#include "exchange/issuer_authority_base.h" +#include "exchange/token_object.h" +#include "medperf/token_object.h" + +// ----------------------------------------------------------------- +// METHOD: initialize_contract +// ----------------------------------------------------------------- +bool initialize_contract(const Environment& env, Response& rsp) +{ + ASSERT_SUCCESS(rsp, ww::exchange::token_object::initialize_contract(env), + "failed to initialize the base contract"); + + return rsp.success(true); +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +contract_method_reference_t contract_method_dispatch_table[] = { + + CONTRACT_METHOD2(get_verifying_key, ww::contract::base::get_verifying_key), + CONTRACT_METHOD2(initialize, ww::medperf::token_object::initialize), + + // issuer methods + CONTRACT_METHOD2(get_asset_type_identifier, ww::exchange::issuer_authority_base::get_asset_type_identifier), + CONTRACT_METHOD2(get_issuer_authority, ww::exchange::issuer_authority_base::get_issuer_authority), + CONTRACT_METHOD2(get_authority, ww::exchange::issuer_authority_base::get_authority), + + // from the attestation contract + CONTRACT_METHOD2(get_contract_metadata, ww::contract::attestation::get_contract_metadata), + CONTRACT_METHOD2(get_contract_code_metadata, ww::contract::attestation::get_contract_code_metadata), + + // use the asset + CONTRACT_METHOD2(get_dataset_info, ww::medperf::token_object::get_dataset_info), + CONTRACT_METHOD2(use_dataset, ww::medperf::token_object::use_dataset), + CONTRACT_METHOD2(get_capability, ww::medperf::token_object::get_capability), + CONTRACT_METHOD2(owner_test, ww::medperf::token_object::owner_test), + CONTRACT_METHOD2(update_policy, ww::medperf::token_object::update_policy), + + // object transfer, escrow & claim methods + CONTRACT_METHOD2(transfer,ww::exchange::token_object::transfer), + CONTRACT_METHOD2(escrow,ww::exchange::token_object::escrow), + CONTRACT_METHOD2(escrow_attestation,ww::exchange::token_object::escrow_attestation), + CONTRACT_METHOD2(release,ww::exchange::token_object::release), + CONTRACT_METHOD2(claim,ww::exchange::token_object::claim), + + { NULL, NULL } +}; diff --git a/medperf-contract/easy_test.sh b/medperf-contract/easy_test.sh new file mode 100755 index 0000000..af361cb --- /dev/null +++ b/medperf-contract/easy_test.sh @@ -0,0 +1,6 @@ +source ${PDO_SOURCE_ROOT}/build/common-config.sh +source ${PDO_INSTALL_ROOT}'/bin/activate' +make -C ${PDO_CONTRACTS_ROOT} +make -C ${PDO_CONTRACTS_ROOT} install +$PDO_CONTRACTS_ROOT/medperf-contract/test/mp_test.sh --host $PDO_HOSTNAME --ledger $PDO_LEDGER_URL +deactivate diff --git a/medperf-contract/etc/guardian_service.toml b/medperf-contract/etc/guardian_service.toml new file mode 100644 index 0000000..c3d115a --- /dev/null +++ b/medperf-contract/etc/guardian_service.toml @@ -0,0 +1,69 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# -------------------------------------------------- +# GuardianService -- general information about the guardian service +# -------------------------------------------------- +[GuardianService] +# Identity is a string used to identify the service in log files +Identity = "${identity}" +HttpPort = 7900 +Host = "${host}" + +# -------------------------------------------------- +# StorageService -- information about passing kv stores +# -------------------------------------------------- +[StorageService] +URL = "http://${host}:7901" +KeyValueStore = "${data}/guardian_service.mdb" +BlockStore = "${data}/guardian_service.mdb" +Identity = "${identity}" +HttpPort = 7901 +Host = "${host}" +GarbageCollectionInterval = 0 +MaxDuration = 0 + +# -------------------------------------------------- +# Keys -- configuration for retrieving service keys +# -------------------------------------------------- +[Key] +SearchPath = [ ".", "./keys", "${keys}" ] +FileName = "${identity}_private.pem" + +# -------------------------------------------------- +# Logging -- configuration of service logging +# -------------------------------------------------- +[Logging] +LogLevel = "INFO" +LogFile = "${logs}/${identity}.log" + +# -------------------------------------------------- +# Data -- names for the various databases +# -------------------------------------------------- +[Data] +EndpointRegistry = "${data}/endpoints.db" +CapabilityKeyStore = "${data}/keystore.db" + +# -------------------------------------------------- +# TokenIssuer -- configuration for TI verification +# -------------------------------------------------- +[TokenIssuer] +LedgerKey = "" +CodeHash = "" +ContractIDs = [] + +# -------------------------------------------------- +# TokenObject -- configuration for TO verification +# -------------------------------------------------- +[TokenObject] diff --git a/medperf-contract/etc/medperf.toml b/medperf-contract/etc/medperf.toml new file mode 100644 index 0000000..9dafa21 --- /dev/null +++ b/medperf-contract/etc/medperf.toml @@ -0,0 +1,19 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------- +# medperf family contract source +# ----------------------------------------------------------------- +[ContractFamily.medperf] +token_object = { source = "${home}/contracts/medperf/_medperf_token_object.b64" } diff --git a/medperf-contract/medperf/contracts/token_object.cpp b/medperf-contract/medperf/contracts/token_object.cpp new file mode 100644 index 0000000..8c921a4 --- /dev/null +++ b/medperf-contract/medperf/contracts/token_object.cpp @@ -0,0 +1,567 @@ +/* Copyright 2024 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +// #include +#include +#include +#include +#include "Dispatch.h" + +#include "Cryptography.h" +#include "KeyValue.h" +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Types.h" +#include "Util.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "contract/attestation.h" +#include "contract/base.h" +#include "exchange/token_object.h" +#include "medperf/token_object.h" + + +static KeyValueStore dataset_TO_store("dataset_TO_store"); +static const std::string dataset_id_KEY("dataset_id"); +static const std::string experiment_id_KEY("experiment_id"); +static const std::string associated_model_ids_KEY("associated_model_ids"); +static const std::string associated_model_tags_KEY("associated_model_tags"); +static const std::string dataset_max_use_count_KEY("dataset_max_use_count"); +static const std::string dataset_current_use_count_KEY("dataset_current_use_count"); +static const std::string pdo_challenge_KEY("pdo_challenge"); + +static const std::string dataset_use_capability_kv_store_encryption_key_KEY("dataset_use_capability_kv_store_encryption_key"); +static const std::string dataset_use_capability_kv_store_root_block_hash_KEY("dataset_use_capability_kv_store_root_block_hash"); +static const std::string dataset_use_capability_kv_store_input_key_KEY("dataset_use_capability_kv_store_input_key"); + +// + +// ----------------------------------------------------------------- +// METHOD: initialize +// +// 1. Store the dataset_id (hash of the dataset) that is tokenized. This information should be generated by the dataset owner. The dataset_id is not cross-checked with the dataset itself for now. +// 2. For testing purposes, the experiment_id is associated with the dataset_token at this step, which implies that the dataset is by default consent to be inferenced by all the models under the associated experiment. +// We will Initilize the token object with no experiment/model association. The association will be done via an additional association method. +// 3. The model_id are also stored in the token object. The model_id is the path to the modelcubes. (So far we assume all model matierials are stored locally) +// +// Note that the token object is intentionally kept generic and not hard-coded to any specific model. +// ------------------------------------------------------------------------------------------------------------- +bool ww::medperf::token_object::initialize(const Message &msg, const Environment &env, Response &rsp) +{ + ASSERT_SENDER_IS_CREATOR(env, rsp); + ASSERT_UNINITIALIZED(rsp); + + // print the message to the log + + + ASSERT_SUCCESS(rsp, msg.validate_schema(DATASET_TO_INIT_PARAM_SCHEMA), + "invalid request, missing required parameters for medperf dataset token object initialization"); + + // Get the params to be stored in dataset_TO_store + const std::string dataset_id_value(msg.get_string("dataset_id")); + + // Store params from msg in dataset_TO_store + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_id_KEY, dataset_id_value), "failed to store dataset_id"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(experiment_id_KEY, "none"), "failed to store experiment_id"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_ids_KEY, "none"), "failed to store associated_model_ids"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_max_use_count_KEY, 0), "failed to store dataset_max_use_count"); + + // Set current use count to 0 + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_current_use_count_KEY, (uint32_t)0), "failed to store dataset_current_use_count"); + + // Do the rest of the initialization of the token object via the initialize method in the exchange contract + ww::value::Structure to_message(TO_INITIALIZE_PARAM_SCHEMA); + + const std::string ledger_verifying_key(msg.get_string("ledger_verifying_key")); + ww::value::Object initialization_package; + msg.get_value("initialization_package", initialization_package); + ww::value::Object asset_authority_chain; + msg.get_value("asset_authority_chain", asset_authority_chain); + + ASSERT_SUCCESS(rsp, to_message.set_string("ledger_verifying_key", ledger_verifying_key.c_str()), "unexpected error: failed to set the parameter"); + ASSERT_SUCCESS(rsp, to_message.set_value("initialization_package", initialization_package), "unexpected error: failed to set the parameter"); + ASSERT_SUCCESS(rsp, to_message.set_value("asset_authority_chain", asset_authority_chain), "unexpected error: failed to set the parameter"); + + return ww::exchange::token_object::initialize(to_message, env, rsp); +} + + +// update_policy attaches the policy +// bool ww::medperf::token_object::update_policy(const Message &msg, const Environment &env, Response &rsp) +// { +// // only the owner can +// ASSERT_SENDER_IS_CREATOR(env, rsp); +// ASSERT_UNINITIALIZED(rsp); +// } + +// ----------------------------------------------------------------- +// METHOD: get_dataset_info +// +// Return fixed model parameters, schema for user-specified model parameters, and +// model metadata useful for the TO to understand how to use the model required to invoke Inference API. +// Method is public, and can be invoked by PDO user. +// note that we are not returning the "remaining use count" as part of the model info. +// ideally a prospective token buyer would like access to "remaining use count" before purchasing the token. +// In such a case, ideally such information shall be provided only after escrow of payment for the token is done. +// Left for future enhancement. +// ----------------------------------------------------------------- + +bool ww::medperf::token_object::get_dataset_info( + const Message &msg, + const Environment &env, + Response &rsp) +{ + ASSERT_INITIALIZED(rsp); + // ww::value::Structure v(DATASET_INFO_SCHEMA); + ww::value::Object v; + + // Get the dataset_id + std::string dataset_id_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id_string), "failed to retrieve dataset_id"); + ASSERT_SUCCESS(rsp, v.set_string("dataset_id", dataset_id_string.c_str()), "failed to set return value for dataset_id"); + + + std::string associated_model_ids_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_ids_KEY, associated_model_ids_string), "failed to retrieve associated_model_ids"); + ASSERT_SUCCESS(rsp, v.set_string("associated_model_ids", associated_model_ids_string.c_str()), "failed to set return value for associated_model_ids"); + + + std::string experiment_id_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(experiment_id_KEY, experiment_id_string), "failed to retrieve experiment_id_string"); + ASSERT_SUCCESS(rsp, v.set_string("experiment_id", experiment_id_string.c_str()), "failed to set return value for experiment_id_string"); + + std::string associated_model_tags_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_tags_KEY, associated_model_tags_string), "failed to retrieve associated_model_tags"); + ASSERT_SUCCESS(rsp, v.set_string("associated_model_tags", associated_model_tags_string.c_str()), "failed to set return value for associated_model_tags"); + + // Get the max use count + uint32_t dataset_max_use_count_value; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to retrieve dataset_max_use_count"); + ASSERT_SUCCESS(rsp, v.set_number("max_use_count", dataset_max_use_count_value), "failed to set return value for max_use_count"); + + return rsp.value(v, false); +} + +// ----------------------------------------------------------------- +// METHOD: use_dataset +// +// 1. Save the parameters required to generate a use_dataset capability to kvs, increments the current use count, and returns the call +// Capability is calculated and returned once proof of commit of state is presented (via the get_capability method). +// 2. Inputs: +// kvstore_encryption_key +// kvstore_root_block_hash +// kvstore_input_key +// model_ids_to_evaluate +// The first 3 parameters provide flexibility to use large inputs for the model via the kv_store attached to the guardian. +// Only TO may invoke method +// ----------------------------------------------------------------- +bool ww::medperf::token_object::use_dataset( + const Message &msg, + const Environment &env, + Response &rsp) +{ + ASSERT_SENDER_IS_OWNER(env, rsp); + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(USE_DATASET_SCHEMA), "invalid request, missing required parameters"); + + const std::string dataset_id(msg.get_string("dataset_id")); + std::string dataset_id_store; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id_store), "fail to retrieve dataset_id"); + ASSERT_SUCCESS(rsp, dataset_id == dataset_id_store, "dataset does not exist"); + + const std::string kvstore_encryption_key(msg.get_string("kvstore_encryption_key")); + const std::string kvstore_root_block_hash(msg.get_string("kvstore_root_block_hash")); + const std::string kvstore_input_key(msg.get_string("kvstore_input_key")); + + + std::string associated_model_ids_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_ids_KEY, associated_model_ids_string), "failed to retrieve associated_model_ids"); + uint32_t associated_model_ids = std::stoi(associated_model_ids_string); + + std::string model_ids_to_evaluate_string(msg.get_string("model_ids_to_evaluate")); + const uint32_t model_ids_to_evaluate = std::stoi(model_ids_to_evaluate_string) - 1; + ASSERT_SUCCESS(rsp, model_ids_to_evaluate < associated_model_ids, "model is not associated with the dataset"); + + std::string associated_model_tags_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_tags_KEY, associated_model_tags_string), "failed to retrieve associated_model_tags"); + ww::types::StringArray associated_model_tags_vec(associated_model_tags_string); + ASSERT_SUCCESS(rsp, associated_model_tags_vec[model_ids_to_evaluate] == '0', "model has already been scheduled or used"); + + uint32_t dataset_current_use_count_value; + uint32_t dataset_max_use_count_value; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_current_use_count_KEY, dataset_current_use_count_value), "failed to retrieve dataset_current_use_count"); + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to retrieve dataset_max_use_count"); + ASSERT_SUCCESS(rsp, dataset_current_use_count_value + 1 <= dataset_max_use_count_value, "max use count is reached, cannot use dataset"); + + // '1' represents that the model is scheduled for evaluation + associated_model_tags_vec[model_ids_to_evaluate] = '1'; + ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_tags_KEY, associated_model_tags_vec.str().c_str()), "failed to update associated_model_tags"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_current_use_count_KEY, dataset_current_use_count_value + 1), "failed to update dataset_current_use_count"); + + // ww::types::StringArray model_ids_to_evaluate_vec(model_ids_to_evaluate); + // uint32_t num_models_to_evaluate = std::count(model_ids_to_evaluate_vec.begin(), model_ids_to_evaluate_vec.end(), ',') + 1; + + // ASSERT_SUCCESS(rsp, num_models_to_evaluate > 0, "invalid request, no models to evaluate"); + + // std::string associated_model_ids_string; + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_ids_KEY, associated_model_ids_string), "failed to retrieve associated_model_ids"); + // ww::types::StringArray associated_model_ids_vec(associated_model_ids_string); + // uint32_t num_associated_models = std::count(associated_model_ids_vec.begin(), associated_model_ids_vec.end(), ',') + 1; + + // std::vector associated_models = stringToVector(associated_model_ids_string); + // std::vector to_evaluate_models = stringToVector(model_ids_to_evaluate); + // std::vector model_index; + // std::string error_message; + // for (std::string to_eva_model_id : to_evaluate_models) + // { + // error_message = to_eva_model_id + " not associated with the dataset"; + // ASSERT_SUCCESS(rsp, std::find(associated_models.begin(), associated_models.end(), to_eva_model_id) != associated_models.end(), error_message.c_str()); + // model_index.push_back(std::find(associated_models.begin(), associated_models.end(), to_eva_model_id) - associated_models.begin()); + // } + + + // uint32_t dataset_current_use_count_value; + // uint32_t dataset_max_use_count_value; + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_current_use_count_KEY, dataset_current_use_count_value), "failed to retrieve dataset_current_use_count"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to retrieve dataset_max_use_count"); + // ASSERT_SUCCESS(rsp, dataset_current_use_count_value + num_models_to_evaluate <= dataset_max_use_count_value, "max use count is reached, cannot use dataset"); + + // // Add the counter if the use_dataset is successful + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_current_use_count_KEY, dataset_current_use_count_value + num_models_to_evaluate), "failed to update dataset_current_use_count"); + + // // change the status of the model_ids to "scheduled" + // // const std::string associated_model_ids_to_be_stored = associated_model_ids_json.dump(); + // const std::string associated_model_ids_to_be_stored = mapToString(associated_model_ids_map); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_ids_KEY, associated_model_ids_to_be_stored), "failed to store associated_model_ids"); + + // store the parameters required to generate a use_dataset capability + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_use_capability_kv_store_encryption_key_KEY, kvstore_encryption_key), "failed to store dataset_use_capability_kv_store_enc_key"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_use_capability_kv_store_root_block_hash_KEY, kvstore_root_block_hash), "failed to store dataset_use_capability_kv_store_hash"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_use_capability_kv_store_input_key_KEY, kvstore_input_key), "failed to store dataset_use_capability_kv_store_input_key"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_use_capability_user_inputs_KEY, user_inputs), "failed to store dataset_use_capability_user_inputs"); + + return rsp.success(true); +} + + + + +// ----------------------------------------------------------------- +// METHOD: get_capability +// +// Check proof of commit, calculate/return capability. +// Only TO may invoke method. It is currently possible for the TO to ask for a past capability +// even after token transfer. This is a feature, not a bug. The justification is that +// any new owner is only getting access to "unused uses" of the model. +// ----------------------------------------------------------------- + +bool ww::medperf::token_object::get_capability( + const Message &msg, + const Environment &env, + Response &rsp) +{ + ASSERT_SENDER_IS_OWNER(env, rsp); + ASSERT_INITIALIZED(rsp); + ASSERT_SUCCESS(rsp, msg.validate_schema(GET_CAPABILITY_SCHEMA), "invalid request, missing required parameters"); + + // Ensure that the current use count is greater than 0, so that an attempt to use the model was made. + // Otherwise, the capability cannot be generated.ls + // uint32_t dataset_current_use_count_value; + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_current_use_count_KEY, dataset_current_use_count_value), "failed to retrieve dataset_current_use_count"); + // ASSERT_SUCCESS(rsp, dataset_current_use_count_value > 0, "invalid request, capability can be obtained only after use_dataset is called"); + + // check for proof of commit of current state of the token object before returning capability + std::string ledger_key; + if (!ww::contract::attestation::get_ledger_key(ledger_key) && ledger_key.length() > 0) + return rsp.error("contract has not been initialized"); + + const std::string ledger_signature(msg.get_string("ledger_signature")); + + ww::types::ByteArray buffer; + std::copy(env.contract_id_.begin(), env.contract_id_.end(), std::back_inserter(buffer)); + std::copy(env.state_hash_.begin(), env.state_hash_.end(), std::back_inserter(buffer)); + + ww::types::ByteArray signature; + if (!ww::crypto::b64_decode(ledger_signature, signature)) + return rsp.error("failed to decode ledger signature"); + if (!ww::crypto::ecdsa::verify_signature(buffer, ledger_key, signature)) + return rsp.error("failed to verify ledger signature"); + + // the current state has been committed so now compute and return the capability + ww::value::Structure params(GENERATE_CAPABILITY_SCHEMA); + + // Get kvstore_encryption_key from dataset_TO_store + std::string kvstore_encryption_key; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_use_capability_kv_store_encryption_key_KEY, kvstore_encryption_key), "failed to retrieve dataset_use_capability_kv_store_enc_key"); + ASSERT_SUCCESS(rsp, params.set_string("kvstore_encryption_key", kvstore_encryption_key.c_str()), "failed to set return value for kvstore_encryption_key"); + + // Get kvstore_root_block_hash from dataset_TO_store + std::string kvstore_root_block_hash; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_use_capability_kv_store_root_block_hash_KEY, kvstore_root_block_hash), "failed to retrieve dataset_use_capability_kv_store_hash"); + ASSERT_SUCCESS(rsp, params.set_string("kvstore_root_block_hash", kvstore_root_block_hash.c_str()), "failed to set return value for kvstore_root_block_hash"); + + // Get kvstore_input_key from dataset_TO_store + std::string kvstore_input_key; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_use_capability_kv_store_input_key_KEY, kvstore_input_key), "failed to retrieve dataset_use_capability_kv_store_input_key"); + ASSERT_SUCCESS(rsp, params.set_string("kvstore_input_key", kvstore_input_key.c_str()), "failed to set return value for kvstore_input_key"); + + // Get dataset_id from dataset_TO_store + std::string dataset_id_store; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id_store), "failed to retrieve dataset_id"); + std::string dataset_id_order(msg.get_string("dataset_id")); + ASSERT_SUCCESS(rsp, dataset_id_store == dataset_id_order, "dataset does not exist"); + ASSERT_SUCCESS(rsp, params.set_string("dataset_id", dataset_id_store.c_str()), "failed to set return value for dataset_id"); + + // Get associated_model_ids from dataset_TO_store + std::string associated_model_tags_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_tags_KEY, associated_model_tags_string), "failed to retrieve associated_model_tags"); + ww::types::StringArray associated_model_tags_vec(associated_model_tags_string); + std::string model_id_to_evaluate_string = ""; + + for (size_t i = 0; i < associated_model_tags_vec.size(); i++) + { + if (associated_model_tags_vec[i] == '1') + { + model_id_to_evaluate_string += std::to_string(i+1) + ","; + associated_model_tags_vec[i] = '2'; + + } + } + model_id_to_evaluate_string.pop_back(); + + ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_tags_KEY, associated_model_tags_vec.str().c_str()), "failed to store associated_model_ids"); + ASSERT_SUCCESS(rsp, params.set_string("model_ids_to_evaluate", model_id_to_evaluate_string.c_str()), "failed to set return value for model_ids_to_evaluate"); + + + // Calculate capability + ww::value::Object result; + ASSERT_SUCCESS(rsp, ww::exchange::token_object::create_operation_package("use_dataset", params, result), + "unexpected error: failed to generate capability"); + + // this assumes that generating the capability changes state + return rsp.value(result, true); +} + +// ----------------------------------------------------------------- +// Method: return a hello world message +// ----------------------------------------------------------------- + +bool ww::medperf::token_object::owner_test( + const Message &msg, + const Environment &env, + Response &rsp) +{ + // check if this is called by the owner + ASSERT_SENDER_IS_OWNER(env, rsp); + ww::value::String result("owner!"); + return rsp.value(result, false); +} + +// ----------------------------------------------------------------- +// Method: check if the sender is the creator +// ----------------------------------------------------------------- + +bool ww::medperf::token_object::update_policy( + const Message &msg, + const Environment &env, + Response &rsp) +{ + // check if this is called by the creator + ASSERT_SENDER_IS_CREATOR(env, rsp); + + // schema validation + ASSERT_SUCCESS(rsp, msg.validate_schema(UPDATE_POLICY_SCHEMA), "invalid request, missing required parameters"); + + ww::value::Structure result(DATASET_INFO_SCHEMA); + + + const std::string dataset_id_value(msg.get_string("dataset_id")); + std::string dataset_id_value_from_store; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id_value_from_store), "failed to retrieve dataset_id"); + ASSERT_SUCCESS(rsp, dataset_id_value_from_store == dataset_id_value, "dataset does not exist"); + ASSERT_SUCCESS(rsp, result.set_string("dataset_id", dataset_id_value.c_str()), "failed to set return value for dataset_id"); + + std::string experiment_id_value(msg.get_string("experiment_id")); + // ASSERT_SUCCESS(rsp, msg.get_string("experiment_id", experiment_id_value), "failed to retrieve experiment_id"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(experiment_id_KEY, experiment_id_value), "failed to store experiment_id"); + ASSERT_SUCCESS(rsp, result.set_string("experiment_id", experiment_id_value.c_str()), "failed to set return value for experiment_id"); + + const std::string associated_model_ids_value(msg.get_string("associated_model_ids")); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_ids_KEY, associated_model_ids_value), "failed to store associated_model_ids"); + ASSERT_SUCCESS(rsp, result.set_string("associated_model_ids", associated_model_ids_value.c_str()), "failed to set return value for associated_model_ids"); + + // size_t model_count; + // ww::types::StringArray associated_model_ids_for_tag_string(associated_model_ids_value); + // model_count = std::count(associated_model_ids_for_tag_string.begin(), associated_model_ids_for_tag_string.end(), ',') + 1; + + size_t model_count = std::stoi(associated_model_ids_value); + const std::string associated_model_tags_value(model_count, '0'); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_tags_KEY, associated_model_tags_value.c_str()), "failed to store associated_model_tags"); + ASSERT_SUCCESS(rsp, result.set_string("associated_model_tags", associated_model_tags_value.c_str()), "failed to set return value for associated_model_tags"); + // ww::value::Array associated_model_ids_array = splitString(associated_model_ids_value.c_str(), ','); + + + // std::vector associated_model_vec = stringToVector(associated_model_ids_value); + // set the associated_model_tag for each model_id in associated_model_ids to '0' + // std::vector associated_model_tags_vec(associated_model_vec.size(), '0'); + // const std::string associated_model_tags_value = vectorToString(associated_model_tags_vec); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_tags_KEY, associated_model_tags_value), "failed to store associated_model_tags"); + + // removed for testing + // ============================================= + // std::vector associated_model_ids = stringToVector(associated_model_ids_value); + // std::map associated_model_ids_map; + // for (auto model_id : associated_model_ids) + // { + // associated_model_ids_map[model_id] = "not used"; + // } + // const std::string associated_model_to_be_stored = mapToString(associated_model_ids_map); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_ids_KEY, associated_model_to_be_stored), "failed to store associated_model_ids"); + + const uint32_t dataset_max_use_count_value = (uint32_t)msg.get_number("max_use_count"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to store dataset_max_use_count"); + ASSERT_SUCCESS(rsp, result.set_number("max_use_count", dataset_max_use_count_value), "failed to set return value for max_use_count"); + // ww::value::String result("Policy updated!"); + + return rsp.value(result, true); + // return rsp.success(true); +} + + + +std::string mapToString(const std::map& myMap) { + std::ostringstream oss; // Create a string stream + oss << "{"; + for (auto it = myMap.begin(); it != myMap.end(); ++it) { + oss << "\"" << it->first << "\": \"" << it->second << "\""; + if (std::next(it) != myMap.end()) { + oss << ", "; // Add a comma, except for the last item + } + } + oss << "}"; + return oss.str(); // Return the formatted string +} + +std::map stringToMap(const std::string& str) { + std::map myMap; + size_t start = 0; + size_t end = 0; + + // Check if the string starts with '{' and ends with '}' + if (str.front() == '{' && str.back() == '}') { + start = 1; // Skip '{' + end = str.size() - 1; // Calculate end position + } else { + return myMap; // Return an empty map if format is incorrect + } + + while (start < end) { + size_t colonPos = str.find(':', start); // Find the position of ':' + size_t keyStart = str.find('"', start) + 1; // Find the key start position + size_t keyEnd = str.find('"', keyStart); // Find the key end position + + if (keyEnd == std::string::npos || colonPos == std::string::npos || keyEnd > colonPos) { + break; // Exit if format is incorrect + } + + std::string key = str.substr(keyStart, keyEnd - keyStart); // Get the key + size_t valueStart = colonPos + 1; // Start position of value + size_t valueEnd = str.find(',', valueStart); // Find the next ',' position + + if (valueEnd == std::string::npos) { + valueEnd = end; // If no ',' found, set to the end + } + + // Remove whitespace after ':' and before the starting quote of the value + size_t valueQuoteStart = str.find('"', valueStart); + size_t valueQuoteEnd = str.find('"', valueQuoteStart + 1); // Find the value end + + std::string value = str.substr(valueQuoteStart + 1, valueQuoteEnd - valueQuoteStart - 1); // Get value + + myMap[key] = value; // Add to the map + start = valueEnd + 1; // Update the start position + } + + return myMap; // Return the constructed map +} + +std::vector stringToVector(const std::string& str) { + std::vector result; + std::string::size_type start = 0; + std::string::size_type end = str.find(','); + + while (end != std::string::npos) { + result.push_back(str.substr(start, end - start)); + start = end + 1; + end = str.find(',', start); + } + result.push_back(str.substr(start)); + + return result; +} + +std::string vectorToString(const std::vector& vec) { + std::string result = "{"; + for (size_t i = 0; i < vec.size(); ++i) { + result += "\"" + vec[i] + "\""; + if (i < vec.size() - 1) { + result += ", "; + } + } + result += "}"; + return result; +} + +// // splitString takes a string and a delimiter character and returns a ww::value::Array +// ww::value::Array splitString(const std::string& str, char delimiter) { +// ww::value::Array result; +// ww::value::Object model_map; +// std::stringstream ss(str); +// std::string item; + +// while (std::getline(ss, item, delimiter)) { +// model_map.set_value(item.c_str(), ww::value::String("not used")); +// result.append_value(model_map); +// } + +// return result; +// } + + +std::vector splitForTags(const std::string& str) { + std::vector result; + std::stringstream ss(str); + std::string item; + + while (std::getline(ss, item, ',')) { + result.push_back('0'); + } + + return result; +} + +std::string joinString(const std::vector& vec, char delimiter) { + std::string result; + for (const auto& item : vec) { + result += item + delimiter; + } + + return result; +} \ No newline at end of file diff --git a/medperf-contract/medperf/token_object.h b/medperf-contract/medperf/token_object.h new file mode 100644 index 0000000..b794b9d --- /dev/null +++ b/medperf-contract/medperf/token_object.h @@ -0,0 +1,113 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "Util.h" +#include "Secret.h" +#include "exchange/token_object.h" +#include "exchange/issuer_authority_base.h" + + +#define DATASET_TO_INIT_PARAM_SCHEMA \ + "{" \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(ledger_verifying_key, "") "," \ + SCHEMA_KWS(initialization_package, CONTRACT_SECRET_SCHEMA) "," \ + SCHEMA_KWS(asset_authority_chain, ISSUER_AUTHORITY_CHAIN_SCHEMA)\ + "}" + +#define DATASET_INFO_SCHEMA \ + "{" \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(experiment_id, "") "," \ + SCHEMA_KW(associated_model_ids, "") "," \ + SCHEMA_KW(associated_model_tags, "") "," \ + SCHEMA_KW(max_use_count, 0) "," \ + "}" + +#define USE_DATASET_SCHEMA \ + "{" \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(model_ids_to_evaluate, "") "," \ + SCHEMA_KW(kvstore_encryption_key, "") "," \ + SCHEMA_KW(kvstore_root_block_hash, "") "," \ + SCHEMA_KW(kvstore_input_key, "") "," \ + "}" + +#define GET_CAPABILITY_SCHEMA \ + "{" \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(ledger_signature,"") \ + "}" + + +#define GENERATE_CAPABILITY_SCHEMA \ + "{" \ + SCHEMA_KW(kvstore_encryption_key, "") "," \ + SCHEMA_KW(kvstore_root_block_hash, "") "," \ + SCHEMA_KW(kvstore_input_key, "") "," \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(model_ids_to_evaluate, "") "," \ + "}" + +#define UPDATE_POLICY_SCHEMA \ + "{" \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(experiment_id, "") "," \ + SCHEMA_KW(associated_model_ids, "") "," \ + SCHEMA_KW(max_use_count, 0) "," \ + "}" + + + +namespace ww +{ +namespace medperf +{ +namespace token_object +{ + // methods + bool initialize(const Message& msg, const Environment& env, Response& rsp); + + bool update_policy(const Message &msg, const Environment &env, Response &rsp); + + // reserved for testing + bool get_dataset_info(const Message& msg, const Environment& env, Response& rsp); + + bool use_dataset(const Message& msg, const Environment& env, Response& rsp); + bool get_capability(const Message& msg, const Environment& env, Response& rsp); + + // reserved for testing + bool owner_test(const Message& msg, const Environment& env, Response& rsp); + bool update_policy(const Message& msg, const Environment& env, Response& rsp); + + // utility functions + const std::string mapToString(const std::map& myMap); + std::map stringToMap(const std::string& str); + std::vector stringToVector(const std::string& str); + std::string vectorToString(const std::vector& vec); + // ww::value::Array splitString(const std::string& str, char delimiter); + std::vector splitString(const std::string& str, char delimiter); + std::string joinString(const std::vector& vec, char delimiter); + std::vector splitForTags(const std::string& str); +}; // token_object +}; // medperf +}; // ww diff --git a/medperf-contract/medperf_common.cmake b/medperf-contract/medperf_common.cmake new file mode 100644 index 0000000..7ec4dde --- /dev/null +++ b/medperf-contract/medperf_common.cmake @@ -0,0 +1,39 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +IF(NOT DEFINED EXCHANGE_INCLUDES) + MESSAGE(FATAL_ERROR "EXCHANGE_INCLUDES is not defined") +ENDIF() + +# --------------------------------------------- +# Set up the include list +# --------------------------------------------- +SET (MEDPERF_INCLUDES ${WASM_INCLUDES}) +LIST(APPEND MEDPERF_INCLUDES ${EXCHANGE_INCLUDES}) +LIST(APPEND MEDPERF_INCLUDES ${CMAKE_CURRENT_LIST_DIR}) + +# --------------------------------------------- +# Set up the default source list +# --------------------------------------------- +FILE(GLOB MEDPERF_COMMON_SOURCE ${CMAKE_CURRENT_LIST_DIR}/medperf/common/*.cpp) +FILE(GLOB MEDPERF_CONTRACT_SOURCE ${CMAKE_CURRENT_LIST_DIR}/medperf/contracts/*.cpp) + +SET (MEDPERF_SOURCES) +LIST(APPEND MEDPERF_SOURCES ${MEDPERF_COMMON_SOURCE}) +LIST(APPEND MEDPERF_SOURCES ${MEDPERF_CONTRACT_SOURCE}) + +# --------------------------------------------- +# Build the wawaka contract common library +# --------------------------------------------- +SET(MEDPERF_LIB ww_medperf) diff --git a/medperf-contract/pdo/medperf/__init__.py b/medperf-contract/pdo/medperf/__init__.py new file mode 100644 index 0000000..c0dfac2 --- /dev/null +++ b/medperf-contract/pdo/medperf/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ 'plugins', 'scripts', 'operations', 'common', 'wsgi', 'resources'] diff --git a/medperf-contract/pdo/medperf/common/__init__.py b/medperf-contract/pdo/medperf/common/__init__.py new file mode 100644 index 0000000..c595226 --- /dev/null +++ b/medperf-contract/pdo/medperf/common/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ + 'capability_keys', + 'capability_keystore', + 'endpoint_registry', + 'guardian_service', + 'secrets', + 'utility', + ] diff --git a/medperf-contract/pdo/medperf/common/capability_keys.py b/medperf-contract/pdo/medperf/common/capability_keys.py new file mode 100644 index 0000000..dbc6cee --- /dev/null +++ b/medperf-contract/pdo/medperf/common/capability_keys.py @@ -0,0 +1,118 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import pdo.common.crypto as crypto +import pdo.common.keys as keys + +import logging +logger = logging.getLogger(__name__) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class CapabilityKeys(keys.ServiceKeys) : + + # ------------------------------------------------------- + @classmethod + def create_new_keys(cls) : + signing_key = crypto.SIG_PrivateKey() + signing_key.Generate() + decryption_key = crypto.PKENC_PrivateKey() + decryption_key.Generate() + + return cls(signing_key, decryption_key) + + # ------------------------------------------------------- + @classmethod + def deserialize(cls, serialized_signing_key, serialized_decryption_key) : + signing_key = crypto.SIG_PrivateKey(serialized_signing_key) + decryption_key = crypto.PKENC_PrivateKey(serialized_decryption_key) + return cls(signing_key, decryption_key) + + # ------------------------------------------------------- + def __init__(self, signing_key, decryption_key) : + super().__init__(signing_key) + self._decryption_key = decryption_key + self._encryption_key = decryption_key.GetPublicKey() + + # ------------------------------------------------------- + @property + def encryption_key(self) : + return self._encryption_key.Serialize() + + # ------------------------------------------------------- + @property + def decryption_key(self) : + return self._decryption_key.Serialize() + + # ------------------------------------------------------- + def serialize(self) : + return (self.signing_key, self.decryption_key) + + # ------------------------------------------------------- + def encrypt(self, message, encoding = 'raw') : + """ + encrypt a message to send privately to the enclave + + :param message: text to encrypt + :param encoding: encoding for the encrypted cipher text, one of raw, hex, b64 + """ + + if type(message) is bytes : + message_byte_array = message + elif type(message) is tuple : + message_byte_array = message + else : + message_byte_array = bytes(message, 'ascii') + + encrypted_byte_array = self._encryption_key.EncryptMessage(message_byte_array) + if encoding == 'raw' : + encoded_bytes = encrypted_byte_array + elif encoding == 'hex' : + encoded_bytes = crypto.byte_array_to_hex(encrypted_byte_array) + elif encoding == 'b64' : + encoded_bytes = crypto.byte_array_to_base64(encrypted_byte_array) + else : + raise ValueError('unknown encoding; {0}'.format(encoding)) + + return encoded_bytes + + # ------------------------------------------------------- + def decrypt(self, message, encoding = 'raw') : + """ + encrypt a message to send privately to the enclave + + :param message: text to encrypt + :param encoding: encoding for the encrypted cipher text, one of raw, hex, b64 + """ + + if type(message) is bytes : + message_byte_array = message + elif type(message) is tuple : + message_byte_array = message + else : + message_byte_array = bytes(message, 'ascii') + + decrypted_byte_array = self._decryption_key.DecryptMessage(message_byte_array) + if encoding == 'raw' : + encoded_bytes = decrypted_byte_array + elif encoding == 'hex' : + encoded_bytes = crypto.byte_array_to_hex(decrypted_byte_array) + elif encoding == 'b64' : + encoded_bytes = crypto.byte_array_to_base64(decrypted_byte_array) + else : + raise ValueError('unknown encoding; {0}'.format(encoding)) + + return encoded_bytes diff --git a/medperf-contract/pdo/medperf/common/capability_keystore.py b/medperf-contract/pdo/medperf/common/capability_keystore.py new file mode 100644 index 0000000..10cf674 --- /dev/null +++ b/medperf-contract/pdo/medperf/common/capability_keystore.py @@ -0,0 +1,57 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import shelve + +from pdo.medperf.common.capability_keys import CapabilityKeys + +import logging +logger = logging.getLogger(__name__) + +class CapabilityKeyStore(object) : + + # ------------------------------------------------------- + def __init__(self, filename = "keystore.db") : + logger.info('create capability store in file %s', filename) + self._keystore = shelve.open(filename, flag='c', writeback=True) + try : + self.mgmt_capability_key = self.get_capability_key('management_capability_key') + except KeyError as ke: + self.mgmt_capability_key = self.create_capability_key('management_capability_key') + + try : + self.svc_capability_key = self.get_capability_key('service_capability_key') + except KeyError as ke: + self.svc_capability_key = self.create_capability_key('service_capability_key') + + # ------------------------------------------------------- + def close(self) : + self._keystore.close() + self._keystore = None + + # ------------------------------------------------------- + def get_capability_key(self, minted_identity) : + (signing_key, decryption_key) = self._keystore[minted_identity] + return CapabilityKeys.deserialize(signing_key, decryption_key) + + # ------------------------------------------------------- + def set_capability_key(self, minted_identity, capability_key) : + (signing_key, decryption_key) = capability_key.serialize() + self._keystore[minted_identity] = (signing_key, decryption_key) + return capability_key + + # ------------------------------------------------------- + def create_capability_key(self, minted_identity) : + capability_key = CapabilityKeys.create_new_keys() + return self.set_capability_key(minted_identity, capability_key) diff --git a/medperf-contract/pdo/medperf/common/endpoint_registry.py b/medperf-contract/pdo/medperf/common/endpoint_registry.py new file mode 100644 index 0000000..297c0c3 --- /dev/null +++ b/medperf-contract/pdo/medperf/common/endpoint_registry.py @@ -0,0 +1,44 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import shelve + +from pdo.common.keys import EnclaveKeys + +import logging +logger = logging.getLogger(__name__) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class EndpointRegistry(object) : + + # ------------------------------------------------------- + def __init__(self, filename = "endpoint.db") : + logger.info('create endpoint registry in file %s', filename) + self._registry = shelve.open(filename, flag='c', writeback=True) + + # ------------------------------------------------------- + def close(self) : + self._registry.close() + self._registry = None + + # ------------------------------------------------------- + def get_endpoint(self, contract_id) : + (verifying_key, encryption_key) = self._registry[contract_id] + return EnclaveKeys(verifying_key, encryption_key) + + # ------------------------------------------------------- + def set_endpoint(self, contract_id, verifying_key, encryption_key) : + self._registry[contract_id] = (verifying_key, encryption_key) + return EnclaveKeys(verifying_key, encryption_key) diff --git a/medperf-contract/pdo/medperf/common/guardian_service.py b/medperf-contract/pdo/medperf/common/guardian_service.py new file mode 100644 index 0000000..d3361b0 --- /dev/null +++ b/medperf-contract/pdo/medperf/common/guardian_service.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Client for the guardian service frontend +""" + +import json +import requests +import time +from urllib.parse import urljoin + +from pdo.service_client.generic import MessageException +from pdo.service_client.generic import GenericServiceClient +from pdo.service_client.storage import StorageServiceClient +import pdo.common.keys as keys + +import logging +logger = logging.getLogger(__name__) + +## ----------------------------------------------------------------- +## CLASS: GuardianServiceClient +## ----------------------------------------------------------------- +class GuardianServiceClient(GenericServiceClient) : + + default_timeout = 20.0 + + # ----------------------------------------------------------------- + def __init__(self, url) : + super().__init__(url) + self.session = requests.Session() + self.session.headers.update({'x-session-identifier' : self.Identifier}) + self.request_identifier = 0 + + service_info = self.get_guardian_metadata() + self.enclave_keys = keys.EnclaveKeys(service_info['verifying_key'], service_info['encryption_key']) + + self.storage_service_url = service_info['storage_service_url'] + self.storage_service_client = StorageServiceClient(self.storage_service_url) + # ensure the local storage service used by the guardian service is running before starting the + # guardian service. + self._attach_storage_service_(self.storage_service_client) + + # ----------------------------------------------------------------- + @property + def verifying_key(self) : + return self.enclave_keys.verifying_key + + # ----------------------------------------------------------------- + @property + def encryption_key(self) : + return self.enclave_keys.encryption_key + + # ------------------------------------------------------- + def _attach_storage_service_(self, storage_service) : + self.storage_service_verifying_key = storage_service.verifying_key + + self.get_block = storage_service.get_block + self.get_blocks = storage_service.get_blocks + self.store_block = storage_service.store_block + self.store_blocks = storage_service.store_blocks + self.check_block = storage_service.check_block + self.check_blocks = storage_service.check_blocks + + # ----------------------------------------------------------------- + def __post_request__(self, path, request) : + + try : + url = urljoin(self.ServiceURL, path) + while True : + response = self.session.post(url, json=request, timeout=self.default_timeout, stream=False) + if response.status_code == 429 : + logger.info('prepare to resubmit the request') + sleeptime = min(1.0, float(response.headers.get('retry-after', 1.0))) + time.sleep(sleeptime) + continue + + response.raise_for_status() + return response.json() + + except (requests.HTTPError, requests.ConnectionError, requests.Timeout) as e : + logger.warn('network error connecting to service (%s); %s', path, str(e)) + raise MessageException(str(e)) from e + + # ----------------------------------------------------------------- + def __get_request__(self, path) : + + try : + url = urljoin(self.ServiceURL, path) + while True : + response = self.session.get(url, timeout=self.default_timeout) + if response.status_code == 429 : + logger.info('prepare to resubmit the request') + sleeptime = min(1.0, float(response.headers.get('retry-after', 1.0))) + time.sleep(sleeptime) + continue + + response.raise_for_status() + return response.json() + + except (requests.HTTPError, requests.ConnectionError, requests.Timeout) as e : + logger.warn('network error connecting to service (%s); %s', path, str(e)) + raise MessageException(str(e)) from e + + # ----------------------------------------------------------------- + def get_guardian_metadata(self) : + return self.__get_request__('info') + + # ----------------------------------------------------------------- + def add_endpoint(self, **params) : + return self.__post_request__('add_endpoint', params) + + # ----------------------------------------------------------------- + def provision_token_issuer(self, **params) : + return self.__post_request__('provision_token_issuer', params) + + # ----------------------------------------------------------------- + def provision_token_object(self, **params) : + return self.__post_request__('provision_token_object', params) + + # ----------------------------------------------------------------- + def process_capability(self, **params) : + return self.__post_request__('process_capability', params) diff --git a/medperf-contract/pdo/medperf/common/secrets.py b/medperf-contract/pdo/medperf/common/secrets.py new file mode 100644 index 0000000..4117ca3 --- /dev/null +++ b/medperf-contract/pdo/medperf/common/secrets.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +import json + +from pdo.medperf.common.utility import ValidateJSON +import pdo.common.crypto as crypto + +import logging +logger = logging.getLogger(__name__) + +__all__ = [ 'recv_secret', 'send_secret' ] + +__secret_schema__ = { + "type" : "object", + "properties" : { + "encrypted_session_key" : { "type" : "string" }, + "session_key_iv" : { "type" : "string" }, + "encrypted_message" : { "type" : "string" }, + }, +} + + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def recv_secret(capability_key, secret) : + """Process an incoming secret + + :param capability_key pdo.medperf.common.capability_keys.CapabilityKeys: decryption key + :param secret str: the secret to be unpacked + :returns dict: the parsed json message in the secret + """ + + if not ValidateJSON(secret, __secret_schema__) : + return None # throw exception? + + encrypted_session_key = crypto.base64_to_byte_array(secret['encrypted_session_key']) + session_key = capability_key.decrypt(encrypted_session_key, encoding='raw') + session_iv = crypto.base64_to_byte_array(secret['session_key_iv']) + cipher = crypto.base64_to_byte_array(secret['encrypted_message']) + raw_message = crypto.SKENC_DecryptMessage(session_key, session_iv, cipher) + message = crypto.byte_array_to_string(raw_message) + + return json.loads(message) + + +# ----------------------------------------------------------------- +# send_secret +# ----------------------------------------------------------------- +def send_secret(capability_key, message) : + """Create a secret for transmission + + :param capability_key pdo.medperf.common.capability_keys.CapabilityKeys: decryption key + :param message dict: dictionary that will be encrypted as JSON in the secret + :returns dict: the secret + """ + + session_key = crypto.SKENC_GenerateKey() + session_iv = crypto.SKENC_GenerateIV() + serialized_message = crypto.string_to_byte_array(json.dumps(message)) + cipher = crypto.SKENC_EncryptMessage(session_key, session_iv, serialized_message) + encrypted_session_key = capability_key.encrypt(session_key) + + result = dict() + result['encrypted_session_key'] = crypto.byte_array_to_base64(encrypted_session_key) + result['session_key_iv'] = crypto.byte_array_to_base64(session_iv) + result['encrypted_message'] = crypto.byte_array_to_base64(cipher) + + return result diff --git a/medperf-contract/pdo/medperf/common/utility.py b/medperf-contract/pdo/medperf/common/utility.py new file mode 100644 index 0000000..f0d53cf --- /dev/null +++ b/medperf-contract/pdo/medperf/common/utility.py @@ -0,0 +1,23 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import jsonschema + +# ----------------------------------------------------------------- +def ValidateJSON(instance, schema): + try: + jsonschema.validate(instance=instance, schema=schema) + except jsonschema.exceptions.ValidationError as err: + return False + return True diff --git a/medperf-contract/pdo/medperf/operations/__init__.py b/medperf-contract/pdo/medperf/operations/__init__.py new file mode 100644 index 0000000..13e2956 --- /dev/null +++ b/medperf-contract/pdo/medperf/operations/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +__all__ = [ 'use_dataset'] + +from pdo.medperf.operations.use_dataset import DataOperation + +capability_handler_map = { + 'use_dataset' : DataOperation, +} diff --git a/medperf-contract/pdo/medperf/operations/use_dataset.py b/medperf-contract/pdo/medperf/operations/use_dataset.py new file mode 100644 index 0000000..658c977 --- /dev/null +++ b/medperf-contract/pdo/medperf/operations/use_dataset.py @@ -0,0 +1,159 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +import hashlib +import random +import yaml +import subprocess +from pdo.medperf.common.utility import ValidateJSON +from pdo.common.key_value import KeyValueStore + +import logging +import requests +import urllib.request +import json +import os +logger = logging.getLogger(__name__) + + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class DataOperation(object) : + # ----------------------------------------------------------------- + __schema__ = { + "type" : "object", + "properties" : { + "kvstore_encryption_key" : { "type" : "string" }, + "kvstore_root_block_hash" : { "type" : "string" }, + "kvstore_input_key" : { "type" : "string" }, + "dataset_id" : { "type" : "string" }, + "experiment_id" : { "type" : "string" }, + "associated_model_ids" : { "type" : "string" }, + } + } + + + # ----------------------------------------------------------------- + def __init__(self, config) : + pass + + # @staticmethod + # def inference_dataset(experiment_id, dataset_id, payload, payload_type='json'): + # """ + # Utility function to query the Hugging Face model and get response. + # If the guardian is deployed in a network with a proxy, the proxy settings are automatically picked up + # as long as the proxy is set in the environment variables. + + # payload_type is either json or binary. If binary, the payload is expected to be a byte string. + # If json, the payload is expected to be a dictionary. binary payloads are used for image/audio inputs, + # while json payloads are used for text inputs (e.g. used while working with language models). + + # For example usages, refer to https://huggingface.co/docs/api-inference/en/detailed_parameters + + # """ + # headers = {"Authorization": f"Bearer {dataset_id}"} + # try: + # if payload_type == 'json': + # response = requests.post(experiment_id, headers=headers, json=payload, proxies=urllib.request.getproxies()) + # elif payload_type == 'binary': + # response = requests.post(experiment_id, headers=headers, data=payload, proxies=urllib.request.getproxies()) + # else: + # logger.error("Invalid payload type. Supported types are 'json' and 'binary'") + # return None + # except Exception as e: + # logger.error(f"Request failed: {e}") + # return None + + # if payload_type == 'binary': + # return json.loads(response.content.decode("utf-8")) + + # return response.json() + + + # ----------------------------------------------------------------- + def __call__(self, params) : + if not ValidateJSON(params, self.__schema__) : + return None + + # get the parameters + kvstore_encryption_key = params['kvstore_encryption_key'] + kvstore_root_block_hash = params['kvstore_root_block_hash'] + # used as a challenge to sign the result for now + kvstore_input_key = params['kvstore_input_key'] + dataset_id = params['dataset_id'] + model_ids_to_evaluate = params['model_ids_to_evaluate'] + result_path = "~/.medperf_test_results" + # if result_path does not exist, create it + if not os.path.exists(result_path): + os.makedirs(result_path) + + # path issue, turned off for now + # mlcube_script_path = "~/test_resource/test.sh" + + + test_message = "Hello from the guardian service." + # put the parameters in test_message and return + # for testing purpose + test_message += f"\nReceived order for:" + test_message += f"\nDataset ID: {dataset_id}" + test_message += f"\nModel IDs : {model_ids_to_evaluate}" + print(test_message) + print("================================================") + print("Evaluating models...") + # split model_ids_to_evaluate into a list of integers + model_ids = model_ids_to_evaluate.split(',') + model_ids = [int(i) for i in model_ids] + + for model in model_ids: + # path issue, turned off for now + # if model == 1 or model == 2: + # print(f"Cubes available for model {model}, running the MLCube script...") + # # run the MLCube script with parameter model + # # os.system(f"bash {mlcube_script_path} {model}") + # output_result = subprocess.run(["bash", mlcube_script_path, str(model)], capture_output=True, text=True) + # print(output_result.stdout) + # else: + print(f"Model {model} not available. Generating simulated data...") + # generate simulated data + sim_data = {'AUC': random.uniform(0.5, 1), 'Accuracy': random.uniform(0.5, 1)} + with open(f"{result_path}/{str(model)}.yaml", 'w') as file: + yaml.dump(sim_data, file) + print(f"Simulated data for model {model} generated successfully.") + + print("================================================") + print("Signing each file with simple SHA256 and challenge...") + # handle each model result + for model in model_ids: + try: + with open(f"{result_path}/{str(model)}.yaml", 'r+') as file: + result_data = yaml.safe_load(file) + hash_result = self.simpleSHA256withChallenge(kvstore_input_key, result_data) + new_data = {"signature": hash_result} + yaml.dump(new_data, file) + print(f"Model {model} result signed successfully.") + except Exception as e: + print(f"Error signing model {model} result: {e}") + continue + return test_message + + def simpleSHA256withChallenge(self, challenge, result_data): + # parse result_data to string + result_data_string = json.dumps(result_data) + return hashlib.sha256(challenge.encode() + result_data_string.encode()).hexdigest() \ No newline at end of file diff --git a/medperf-contract/pdo/medperf/plugins/__init__.py b/medperf-contract/pdo/medperf/plugins/__init__.py new file mode 100644 index 0000000..b50ddae --- /dev/null +++ b/medperf-contract/pdo/medperf/plugins/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ 'medperf_guardian', 'medperf_token_object'] diff --git a/medperf-contract/pdo/medperf/plugins/medperf_guardian.py b/medperf-contract/pdo/medperf/plugins/medperf_guardian.py new file mode 100644 index 0000000..4759f6e --- /dev/null +++ b/medperf-contract/pdo/medperf/plugins/medperf_guardian.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +medperf guardian plugins +""" + +import json +import logging +logger = logging.getLogger(__name__) + +import pdo.client.builder.command as pcommand +import pdo.client.builder.contract as pcontract +import pdo.client.builder.shell as pshell +from pdo.client.builder import invocation_parameter + +from pdo.medperf.common.guardian_service import GuardianServiceClient + +__all__ = [ + 'op_provision_token_issuer', + 'op_provision_token_object', + 'op_process_capability', + 'op_add_endpoint', + 'cmd_provision_token_issuer', + 'cmd_provision_token_object', + 'do_medperf_guardian', + 'do_medperf_guardian_service', + 'load_commands', +] + +## ----------------------------------------------------------------- +## add_endpoint +## ----------------------------------------------------------------- +class op_add_endpoint(pcontract.contract_op_base) : + + name = "add_endpoint" + help = "add an attested contract object endpoint to the contract" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-c', '--code-metadata', + help='contract code metadata', + type=invocation_parameter, + required=True) + subparser.add_argument( + '-i', '--contract-id', + help='contract identifier', + type=str, required=True) + subparser.add_argument( + '-l', '--ledger-attestation', + help='attestation from the ledger', + type=invocation_parameter, + required=True) + subparser.add_argument( + '-m', '--contract-metadata', + help='contract metadata', + type=invocation_parameter, + required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the guardian service', + type=str, required=True) + + @classmethod + def invoke(cls, state, session_params, contract_id, ledger_attestation, contract_metadata, code_metadata, url, **kwargs) : + + params = dict() + params['contract_id'] = contract_id + params['ledger_attestation'] = ledger_attestation + params['contract_metadata'] = contract_metadata + params['contract_code_metadata'] = code_metadata + + service_client = GuardianServiceClient(url) + result = service_client.add_endpoint(**params) + + return result + +## ----------------------------------------------------------------- +## provision_token_issuer +## ----------------------------------------------------------------- +class op_provision_token_issuer(pcontract.contract_op_base) : + + name = "provision_token_issuer" + help = "provision the token issuer with the capability management key" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-i', '--contract-id', + help='contract identifier', + type=str, required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the medperf guardian service', + type=str, required=True) + + @classmethod + def invoke(cls, state, session_params, contract_id, url, **kwargs) : + params = dict() + params['contract_id'] = contract_id + + service_client = GuardianServiceClient(url) + raw_result = service_client.provision_token_issuer(**params) + result = json.dumps(raw_result) + return result + +## ----------------------------------------------------------------- +## provision_token_object +## ----------------------------------------------------------------- +class op_provision_token_object(pcontract.contract_op_base) : + + name = "provision_token_object" + help = "provision a token object with an identity and capability generation key" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-p', '--provisioning-package', + help='contract secret containing the provisioning package', + type=invocation_parameter, + required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the medperf guardian service', + type=str, required=True) + + @classmethod + def invoke(cls, state, session_params, provisioning_package, url, **kwargs) : + params = provisioning_package + + service_client = GuardianServiceClient(url) + raw_result = service_client.provision_token_object(**params) + result = json.dumps(raw_result) + return result + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_process_capability(pcontract.contract_op_base) : + + name = "process_capability" + help = "" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-c', '--capability', + help='capability generated by the token object create operation interface', + required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the medperf guardian service', + type=str, required=True) + + @classmethod + def invoke(cls, state, session_params, capability, url, **kwargs) : + params = capability + + service_client = GuardianServiceClient(url) + raw_result = service_client.process_capability(**params) + result = json.dumps(raw_result) + return result + +# ----------------------------------------------------------------- +# provision a token issuer +# ----------------------------------------------------------------- +class cmd_provision_token_issuer(pcommand.contract_command_base) : + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-c', '--code-metadata', + help='contract code metadata', + required=True) + subparser.add_argument( + '-i', '--contract-id', + help='contract identifier', + type=str, required=True) + subparser.add_argument( + '-l', '--ledger-attestation', + help='attestation from the ledger', + required=True) + subparser.add_argument( + '-m', '--contract-metadata', + help='contract metadata', + required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the medperf guardian service', + type=str) + + @classmethod + def invoke(cls, state, context, contract_id, ledger_attestation, contract_metadata, code_metadata, url=None, **kwargs) : + + if url is None : + url = context['url'] + + session = None + pcontract.invoke_contract_op( + op_add_endpoint, + state, context, session, + contract_id, + ledger_attestation, + contract_metadata, + code_metadata, + url) + + provisioning_package = pcontract.invoke_contract_op( + op_provision_token_issuer, + state, context, session, + contract_id, + url) + + cls.display('provisioned guardian for token issuer {}'.format(url)) + return json.loads(provisioning_package) + +# ----------------------------------------------------------------- +# provision a token object +# ----------------------------------------------------------------- +class cmd_provision_token_object(pcommand.contract_command_base) : + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-p', '--provisioning-package', + help='contract secret containing the provisioning package', + required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the medperf guardian service', + type=str, required=True) + + @classmethod + def invoke(cls, state, context, provisioning_package, url=None, **kwargs) : + + if url is None : + url = context['url'] + + session = None + to_package = pcontract.invoke_contract_op( + op_provision_token_object, + state, context, session, + provisioning_package, + url, + **kwargs) + + cls.display('provisioned token object for guardian {}'.format(url)) + return json.loads(to_package) + + +## ----------------------------------------------------------------- +## Create the generic, shell independent version of the aggregate command +## ----------------------------------------------------------------- +__operations__ = [ + op_provision_token_issuer, + op_provision_token_object, + op_process_capability, + op_add_endpoint, +] + +do_medperf_guardian_service = pcontract.create_shell_command('medperf_guardian_service', __operations__) + +__commands__ = [ + cmd_provision_token_issuer, + cmd_provision_token_object, +] + +do_medperf_guardian = pcommand.create_shell_command('medperf_guardian', __commands__) + +## ----------------------------------------------------------------- +## Enable binding of the shell independent version to a pdo-shell command +## ----------------------------------------------------------------- +def load_commands(cmdclass) : + pshell.bind_shell_command(cmdclass, 'medperf_guardian', do_medperf_guardian) + pshell.bind_shell_command(cmdclass, 'medperf_guardian_service', do_medperf_guardian_service) \ No newline at end of file diff --git a/medperf-contract/pdo/medperf/plugins/medperf_token_object.py b/medperf-contract/pdo/medperf/plugins/medperf_token_object.py new file mode 100644 index 0000000..77ad9dc --- /dev/null +++ b/medperf-contract/pdo/medperf/plugins/medperf_token_object.py @@ -0,0 +1,673 @@ +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +import time +import sqlite3 + +from pdo.submitter.create import create_submitter +from pdo.contract import invocation_request +from pdo.common.key_value import KeyValueStore +import pdo.common.utility as putils +import pdo.common.crypto as crypto + +import pdo.client.builder as pbuilder +import pdo.client.builder.command as pcommand +import pdo.client.builder.contract as pcontract +import pdo.client.builder.shell as pshell +import pdo.client.commands.contract as pcontract_cmd + +import pdo.exchange.plugins.token_object as token_object +from pdo.common.keys import ServiceKeys + + +from pdo.medperf.common.guardian_service import GuardianServiceClient + +__all__ = [ + 'op_initialize', + 'op_get_verifying_key', + 'op_get_contract_metadata', + 'op_get_contract_code_metadata', + 'op_get_asset_type_identifier', + 'op_get_issuer_authority', + 'op_get_authority', + 'op_transfer', + 'op_escrow', + 'op_release', + 'op_claim', + 'op_get_dataset_info', + 'op_use_dataset', + 'op_get_capability', + 'op_owner_test', + 'op_update_policy', + 'cmd_mint_dataset_tokens', + 'cmd_mint_tokens', + 'cmd_transfer_assets', + 'cmd_use_dataset', + 'cmd_get_dataset_info', + 'cmd_owner_test', + 'cmd_experiment_order', + 'cmd_update_policy', + 'do_medperf_token', + 'do_medperf_token_contract', + 'load_commands', +] + +## ----------------------------------------------------------------- +## inherited operations +## ----------------------------------------------------------------- +op_get_verifying_key = token_object.op_get_verifying_key +op_get_contract_metadata = token_object.op_get_contract_metadata +op_get_contract_code_metadata = token_object.op_get_contract_code_metadata +op_get_asset_type_identifier = token_object.op_get_asset_type_identifier +op_get_issuer_authority = token_object.op_get_issuer_authority +op_get_authority = token_object.op_get_authority +op_transfer = token_object.op_transfer +op_escrow = token_object.op_escrow +op_release = token_object.op_release +op_claim = token_object.op_claim +cmd_mint_tokens = token_object.cmd_mint_tokens +cmd_transfer_assets = token_object.cmd_transfer_assets + +logger = logging.getLogger(__name__) + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_initialize(pcontract.contract_op_base) : + + name = "initialize" + help = "initialize the token object with the package received from the data guardian" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-a', '--authority', + help='serialized authority from the vetting organization', + type=pbuilder.invocation_parameter, required=True) + subparser.add_argument( + '-i', '--initialization-package', + help="the token issuer initialization package from the guardian", + type=pbuilder.invocation_parameter, required=True) + subparser.add_argument( + '-l', '--ledger-key', + help='ledger verifying key', + type=pbuilder.invocation_parameter, required=True) + subparser.add_argument('--dataset_id', help='Name of the contract class for the given dataset', type=str) + # subparser.add_argument('--experiment_id', help='Experiments to be teseted on the dataset', type=str) + # subparser.add_argument('--associated_model_ids', help='Models to be tested on the dataset', type=str) + # # subparser.add_argument('--user_inputs_schema', help='Name of the provisioning service group to use', type=str) + # # subparser.add_argument('--payload_type', help='Name of the storage service group to use', type=str) + # # subparser.add_argument('--medperf_usage_info', help='File that contains contract source code', type=str) + # subparser.add_argument('--max_use_count', help='File that contains contract source code', type=int) + + + @classmethod + def invoke(cls, state, session_params, ledger_key, initialization_package, authority, **kwargs) : + session_params['commit'] = True + params = {} + params['ledger_verifying_key'] = ledger_key + params['initialization_package'] = initialization_package + params['asset_authority_chain'] = authority + + # Add params from kwargs + params['dataset_id'] = kwargs.get('dataset_id') + # params['experiment_id'] = kwargs.get('experiment_id') + # params['associated_model_ids'] = kwargs.get('associated_model_ids') + # params['user_inputs_schema'] = kwargs.get('user_inputs_schema') + # params['payload_type'] = kwargs.get('payload_type', 'json') + # params['medperf_usage_info'] = kwargs.get('medperf_usage_info') + # params['max_use_count'] = kwargs.get('max_use_count', 1) + + + # parse the message (python dict as the defined schema) and call + message = invocation_request('initialize', **params) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_get_dataset_info(pcontract.contract_op_base) : + """op_get_dataset_info implements the method to get the info about the dataset stored in the token object + """ + + name = "op_get_dataset_info" + help = "get info about the dataset stored in the token object" + + @classmethod + def invoke(cls, state, session_params, **kwargs) : + session_params['commit'] = False + + params = {} + message = invocation_request('get_dataset_info', **params) + # try: + # result = pcontract_cmd.send_to_contract(state, message, **session_params) + # except Exception as e: + # cls.display_error("get_dataset_info method evaluation failed. {}".format(e)) + # return None + result = pcontract_cmd.send_to_contract(state, message, **session_params) + + cls.log_invocation(message, result) + + return result + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_use_dataset(pcontract.contract_op_base) : + """op_use_dataset implements step 1 for the execution of MedPerf experiment via the guardian service + The specific operation depends on the model. This method is used to fix all the parameters required to generate + the capability that will be sent to the guardian service. the capability itself is not returned by this method + the capability is returned only after proof of commit is received from the ledger after this method. + Step 2 obtains the capability from the token object and sends it to the guardian service. + """ + + name = "op_use_dataset" + help = "implement step 1 of execution of MedPerf experiment" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '--kvstore_encryption_key', + help='Encryption key for the KV store if payload is binary', + type=str) + + subparser.add_argument( + '--kvstore_input_key', + help='Data file input key for the KV store if payload is binary', + type=str) + + subparser.add_argument( + '--kvstore_root_block_hash', + help='KV store hashed identity if payload is binary', + type=str) + + # subparser.add_argument( + # '--dataset_id', + # help='Name of the contract class for the given dataset', + # type=str) + + # subparser.add_argument( + # '--model_ids_to_evaluate', + # help='Models to be tested on the dataset', + # type=str) + + @classmethod + def invoke(cls, state, session_params, kvstore_encryption_key, kvstore_input_key, kvstore_root_block_hash, dataset_id, model_ids_to_evaluate, **kwargs) : + session_params['commit'] = True + + # send the request to the contract to create a capability for the guardian + params = {} + params['kvstore_encryption_key'] = kvstore_encryption_key + params['kvstore_input_key'] = kvstore_input_key + params['kvstore_root_block_hash'] = kvstore_root_block_hash + params['dataset_id'] = dataset_id + params['model_ids_to_evaluate'] = model_ids_to_evaluate + # params['user_inputs'] = user_inputs + + message = invocation_request('use_dataset', **params) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + + # try: + # result = pcontract_cmd.send_to_contract(state, message, **session_params) + # except Exception as e: + # raise + + cls.log_invocation(message, result) + return result + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_get_capability(pcontract.contract_op_base) : + """op_get_capability implements step 2 for the execution of MedPerf experiment via the guardian service + This method carries proof of commit from ledger as input, and obtains the capability from the token object. + Step 1 fixes all the parameters required to generate the capability. + """ + + name = "op_get_capability" + help = "implement step 2 of execution of MedPerf experiment" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-l', '--ledger-attestation', + help='attestation from the ledger that the current state of the token issuer is committed', + type=pbuilder.invocation_parameter, required=True) + + subparser.add_argument( + '--dataset_id', + help='Name of the contract class for the given dataset', + type=str) + + @classmethod + def invoke(cls, state, session_params, ledger_attestation, **kwargs) : + session_params['commit'] = False + + params = {} + params['ledger_signature'] = ledger_attestation + params['dataset_id'] = kwargs.get('dataset_id') + + message = invocation_request('get_capability', **params) + capability = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, capability) + + return capability + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class op_owner_test(pcontract.contract_op_base) : + """op_owner_test implements a simple hello world operation + """ + + name = "owner_test" + help = "simple hello world operation" + + # # For this operation, we don't need any arguments + # @classmethod + # def add_arguments(cls, subparser) : + # pass + + # This operation doesn't require any arguments + @classmethod + def invoke(cls, state, session_params, **kwargs) : + session_params['commit'] = False + + params = {} + + message = invocation_request('owner_test', **params) + + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_owner_test(pcommand.contract_command_base) : + """cmd_owner_test implements a simple hello world command + """ + + name = "owner_test" + help = "simple hello world command" + + @classmethod + def invoke(cls, state, context, **kwargs) : + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError("token has not been created") + + session = pbuilder.SessionParameters(save_file=save_file) + # invoke the owner_test operation + result = pcontract.invoke_contract_op( + op_owner_test, + state, context, session, + **kwargs) + # try: + # result = pcontract.invoke_contract_op( + # op_owner_test, + # state, context, session, + # **kwargs) + # except Exception as e: + # cls.display_error("op_owner_test method evaluation failed. {}".format(e)) + # return None + cls.display(result) + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_update_policy(pcontract.contract_op_base) : + """op_update_policy implements a simple hello world operation + """ + + name = "update_policy" + help = "simple hello world operation" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '--dataset_id', + help='Name of the contract class for the given dataset', + type=str) + + subparser.add_argument( + '--experiment_id', + help='Experiments to be teseted on the dataset', + type=str) + + + subparser.add_argument( + '--associated_model_ids', + help='Models to be tested on the dataset', + type=str) + + subparser.add_argument( + '--max_use_count', + help='Max number of times the dataset can be used', + type=int) + + @classmethod + def invoke(cls, state, session_params, **kwargs) : + session_params['commit'] = False + + params = {} + params['dataset_id'] = kwargs.get('dataset_id') + params['experiment_id'] = kwargs.get('experiment_id') + params['associated_model_ids'] = kwargs.get('associated_model_ids') + params['max_use_count'] = kwargs.get('max_use_count') + + message = invocation_request('update_policy', **params) + + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_use_dataset(pcommand.contract_command_base) : + """cmd_use_dataset implements implements the usage of the dataset via the guardian service + """ + name = "use_dataset" + help = "run experiment on the dataset" + + @classmethod + def add_arguments(cls, subparser) : + + subparser.add_argument( + '--dataset_id', + help='Name of the contract class for the given dataset', + type=str) + + subparser.add_argument( + '--model_ids_to_evaluate', + help='Models to be tested on the dataset', + type=str) + + + @classmethod + def invoke(cls, state, context, **kwargs) : + + # ensure token has been created + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError("token has not been created") + + # query the token object for model info, get the payload type, and ensure that for binary payloads + # data file is provided, and for json payloads, user inputs are provided + session = pbuilder.SessionParameters(save_file=save_file) + # model_info_json = pcontract.invoke_contract_op( + # op_get_dataset_info, + # state, context, session, + # **kwargs) + + + kvstore_encryption_key = "not_used" + kvstore_input_key = "this_is_a_test_string_as_challenge" + kvstore_root_block_hash = "not_used" + try: + result = pcontract.invoke_contract_op( + op_use_dataset, + state, context, session, + kvstore_encryption_key, + kvstore_input_key, + kvstore_root_block_hash, + **kwargs) + except Exception as e: + cls.display_error("op_use_dataset method evaluation failed. {}".format(e)) + return None + + # get proof of commit from the ledger + time.sleep(2) # wait for the ledger to commit the transaction, not sure if any wait is needed or the + # correct solution is to poll the ledger until the transaction is committed + + return result + + # to_contract = pcontract_cmd.get_contract(state, save_file) + # ledger_submitter = create_submitter(state.get(['Ledger'])) + # state_attestation = ledger_submitter.get_current_state_hash(to_contract.contract_id) + + # # get capability from the token object + # capability = pcontract.invoke_contract_op ( + # op_get_capability, + # state, context, session, + # state_attestation['signature'], + # **kwargs) + + # # push data file to storage service associated with the guardian if payload is binary + # guardian_context = context.get_context('data_guardian_context') + # url = guardian_context['url'] + # service_client = GuardianServiceClient(url) + # # send capability to guardian service + # capability = json.loads(capability) + # result = service_client.process_capability(**capability) + # cls.display(result) + # return result + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_mint_dataset_tokens(pcommand.contract_command_base) : + """Mint token objects + Wrapper around the mint_tokens operation from exchange.plugins.token_object + to be able to specify additional arguments during initialization + """ + + name = "mint_dataset_tokens" + help = "mint tokens for a token issuer" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument('--dataset_id', help='Name of the contract class for the given dataset', type=str) + + @classmethod + def invoke(cls, state, context, **kwargs) : + return pcommand.invoke_contract_cmd(cmd_mint_tokens, state, context, **kwargs) + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_get_dataset_info(pcommand.contract_command_base) : + """cmd_get_dataset_info gets the model info for which token has been created + the model info is entirely obtained from the token object, and does not involve calls to the + guardian + """ + name = "get_dataset_info" + help = "get dataset info stored in the token object" + + @classmethod + def invoke(cls, state, context, **kwargs) : + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError("token has not been created") + + session = pbuilder.SessionParameters(save_file=save_file) + result = pcontract.invoke_contract_op( + op_get_dataset_info, + state, context, session, + **kwargs) + + cls.display(result) + + return result + +class cmd_update_policy(pcommand.contract_command_base) : + """cmd_update_policy implements a simple hello world command + """ + + name = "update_policy" + help = "simple hello world command" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '--dataset_id', + help='Name of the contract class for the given dataset', + type=str) + + subparser.add_argument( + '--experiment_id', + help='Experiments to be teseted on the dataset', + type=str) + + subparser.add_argument( + '--associated_model_ids', + help='Models to be tested on the dataset', + type=str) + + subparser.add_argument( + '--max_use_count', + help='Max number of times the dataset can be used', + type=int) + + @classmethod + def invoke(cls, state, context, **kwargs) : + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError("token has not been created") + + session = pbuilder.SessionParameters(save_file=save_file) + # invoke the owner_test operation + # result = pcontract.invoke_contract_op( + # op_update_policy, + # state, context, session, + # **kwargs) + try: + result = pcontract.invoke_contract_op( + op_update_policy, + state, context, session, + **kwargs) + except Exception as e: + cls.display_error("op_update_policy method evaluation failed. {}".format(e)) + return None + + cls.display(result) + + +class cmd_experiment_order(pcommand.contract_command_base) : + """cmd_experiment_order implements a simple hello world command + """ + + name = "experiment_order" + help = "simple hello world command" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '--dataset_id', + help='Name of the contract class for the given dataset', + type=str) + + + + @classmethod + def invoke(cls, state, context, **kwargs) : + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError("token has not been created") + + session = pbuilder.SessionParameters(save_file=save_file) + + to_contract = pcontract_cmd.get_contract(state, save_file) + ledger_submitter = create_submitter(state.get(['Ledger'])) + state_attestation = ledger_submitter.get_current_state_hash(to_contract.contract_id) + + # get capability from the token object + capability = pcontract.invoke_contract_op ( + op_get_capability, + state, context, session, + state_attestation['signature'], + **kwargs) + + medperf_sqlite_path = kwargs.get('medperf_sqlite_path') + try: + # simulating the posting of the order to the MedPerf server + data_json = {} + data_json["order"] = json.dumps(capability) + print(data_json) + conn = sqlite3.connect(medperf_sqlite_path) + cur = conn.cursor() + # insert new result record to the table + for i in range(10): + cur.execute("update dataset_dataset set user_metadata = ? where id = ?", (json.dumps(data_json), 1)) + conn.commit() + conn.close() + except Exception as e: + print("Error in posting order to MedPerf server") + print(e) + conn.close() + + input("Posting the packed work order to MedPerf Server, now the dataset owner can download the encrypted workorder from Medperf.\n For simplicity, forwarding directly to the guardian.\nPress Enter to continue...") + + + guardian_context = context.get_context('data_guardian_context') + url = guardian_context['url'] + service_client = GuardianServiceClient(url) + # send capability to guardian service + capability = json.loads(capability) + result = service_client.process_capability(**capability) + cls.display(result) + return result + + +## ----------------------------------------------------------------- +## Create the generic, shell independent version of the aggregate command +## ----------------------------------------------------------------- +__operations__ = [ + op_initialize, + op_get_verifying_key, + op_get_contract_metadata, + op_get_contract_code_metadata, + op_get_asset_type_identifier, + op_get_issuer_authority, + op_get_authority, + op_transfer, + op_escrow, + op_release, + op_claim, + op_use_dataset, + op_get_dataset_info, + op_get_capability, + op_owner_test, + op_update_policy, +] + +do_medperf_token_contract = pcontract.create_shell_command('medperf_token_contract', __operations__) + +__commands__ = [ + cmd_mint_dataset_tokens, + cmd_transfer_assets, + cmd_use_dataset, + cmd_get_dataset_info, + cmd_owner_test, + cmd_update_policy, + cmd_experiment_order, +] + +do_medperf_token = pcommand.create_shell_command('medperf_token', __commands__) + +## ----------------------------------------------------------------- +## Enable binding of the shell independent version to a pdo-shell command +## ----------------------------------------------------------------- +def load_commands(cmdclass) : + pshell.bind_shell_command(cmdclass, 'medperf_token', do_medperf_token) + pshell.bind_shell_command(cmdclass, 'medperf_token_contract', do_medperf_token_contract) + \ No newline at end of file diff --git a/medperf-contract/pdo/medperf/resources/__init__.py b/medperf-contract/pdo/medperf/resources/__init__.py new file mode 100644 index 0000000..d0b2a5a --- /dev/null +++ b/medperf-contract/pdo/medperf/resources/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = ['resources'] diff --git a/medperf-contract/pdo/medperf/resources/resources.py b/medperf-contract/pdo/medperf/resources/resources.py new file mode 100644 index 0000000..06483c1 --- /dev/null +++ b/medperf-contract/pdo/medperf/resources/resources.py @@ -0,0 +1,18 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pdo.client.builder.installer as pinstaller + +def install_medperf_contract_resources() : + pinstaller.install_plugin_resources('pdo.medperf.resources', 'medperf') diff --git a/medperf-contract/pdo/medperf/scripts/__init__.py b/medperf-contract/pdo/medperf/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/medperf-contract/pdo/medperf/scripts/guardianCLI.py b/medperf-contract/pdo/medperf/scripts/guardianCLI.py new file mode 100644 index 0000000..51cbe47 --- /dev/null +++ b/medperf-contract/pdo/medperf/scripts/guardianCLI.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Data guardian service. +""" + +import os +import sys +import argparse + +import signal + +import pdo.common.config as pconfig +import pdo.common.logger as plogger +import pdo.common.utility as putils + +from pdo.common.wsgi import AppWrapperMiddleware +from pdo.medperf.wsgi import wsgi_operation_map +from pdo.medperf.common.capability_keystore import CapabilityKeyStore +from pdo.medperf.common.endpoint_registry import EndpointRegistry + +import logging +logger = logging.getLogger(__name__) + + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +from twisted.web import http +from twisted.web.resource import Resource, NoResource +from twisted.web.server import Site +from twisted.python.threadpool import ThreadPool +from twisted.internet import reactor, defer +from twisted.internet.endpoints import TCP4ServerEndpoint +from twisted.web.wsgi import WSGIResource + +## ---------------------------------------------------------------- +def ErrorResponse(request, error_code, msg) : + """Generate a common error response for broken requests + """ + + result = "" + if request.method != 'HEAD' : + result = msg + '\n' + result = result.encode('utf8') + + request.setResponseCode(error_code) + request.setHeader(b'Content-Type', b'text/plain') + request.setHeader(b'Content-Length', len(result)) + request.write(result) + + try : + request.finish() + except : + logger.exception("exception during request finish") + raise + + return request + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def __shutdown__(*args) : + logger.warn('shutdown request received') + reactor.callLater(1, reactor.stop) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def TestService(config) : + """Test for the existance of a guardian service with the current configuration + """ + + from pdo.service_client.generic import MessageException + from pdo.medperf.common.guardian_service import GuardianServiceClient + + try : + http_port = config['GuardianService']['HttpPort'] + http_host = config['GuardianService']['Host'] + service_url = 'http://{}:{}'.format(http_host, http_port) + except KeyError as ke : + logger.error('missing configuration for %s', str(ke)) + sys.exit(-1) + + try : + service_client = GuardianServiceClient(service_url) + except MessageException as m : + # if the error is a message exception then the message stays as info + # since the point of this routine is to test and this means the test + # failed + logger.info('failed to contact guardian service; {}'.format(str(m))) + sys.exit(-1) + except Exception as e : + # if the exception is something more serious, then show the error + # message + logger.error('failed to contact guardian service; {}'.format(str(e))) + sys.exit(-1) + + logger.info('guardian service running; {}'.format(service_url)) + sys.exit(0) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def StartService(config, capability_keystore, endpoint_registry) : + try : + http_port = config['GuardianService']['HttpPort'] + http_host = config['GuardianService']['Host'] + worker_threads = config['GuardianService'].get('WorkerThreads', 8) + reactor_threads = config['GuardianService'].get('ReactorThreads', 8) + except KeyError as ke : + logger.error('missing configuration for %s', str(ke)) + sys.exit(-1) + + logger.info('service started on %s:%s', http_host, http_port) + + thread_pool = ThreadPool(minthreads=1, maxthreads=worker_threads) + thread_pool.start() + reactor.addSystemEventTrigger('before', 'shutdown', thread_pool.stop) + + root = Resource() + for (wsgi_verb, wsgi_app) in wsgi_operation_map.items() : + logger.info('add handler for %s', wsgi_verb) + verb = wsgi_verb.encode('utf8') + app = AppWrapperMiddleware(wsgi_app(config, capability_keystore, endpoint_registry)) + root.putChild(verb, WSGIResource(reactor, thread_pool, app)) + + site = Site(root, timeout=60) + site.displayTracebacks = True + + reactor.suggestThreadPoolSize(reactor_threads) + + signal.signal(signal.SIGQUIT, __shutdown__) + signal.signal(signal.SIGTERM, __shutdown__) + + endpoint = TCP4ServerEndpoint(reactor, http_port, backlog=32, interface=http_host) + endpoint.listen(site) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def RunService(capability_keystore, endpoint_registry) : + @defer.inlineCallbacks + def shutdown_twisted(): + logger.info("Stopping Twisted") + yield reactor.callFromThread(reactor.stop) + + reactor.addSystemEventTrigger('before', 'shutdown', shutdown_twisted) + + try : + reactor.run() + except ReactorNotRunning: + logger.warn('shutdown') + except : + logger.warn('shutdown') + + capability_keystore.close() + endpoint_registry.close() + sys.exit(0) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def LocalMain(config) : + + # load and initialize the model and service keys + try : + logger.debug('initialize the service') + + try : + keystore_filename = config['Data']['CapabilityKeyStore'] + except KeyError as ke : + logger.error('missing required configuration; %s', str(ke)) + sys.exit(-1) + + keystore_filename = putils.build_file_name(keystore_filename, extension='db') + capability_keystore = CapabilityKeyStore(keystore_filename) + + try : + endpoint_filename = config['Data']['EndpointRegistry'] + except KeyError as ke : + logger.error('missing required configuration; %s', str(ke)) + sys.exit(-1) + + endpoint_filename = putils.build_file_name(endpoint_filename, extension='db') + endpoint_registry = EndpointRegistry(endpoint_filename) + + except Exception as e : + logger.exception('failed to initialize service keys; %s', e) + sys.exit(-1) + + # set up the handlers for the enclave service + try : + StartService(config, capability_keystore, endpoint_registry) + except Exception as e: + logger.exception('failed to start the enclave service; %s', e) + sys.exit(-1) + + # and run the service + RunService(capability_keystore, endpoint_registry) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def Main() : + config_map = pconfig.build_configuration_map() + + # parse out the configuration file first + conffiles = [ 'guardian_service.toml' ] + confpaths = [ ".", "./etc", config_map['etc'] ] + + parser = argparse.ArgumentParser() + + # allow for override of bindings in the config map + parser.add_argument('-b', '--bind', help='Define variables for configuration and script use', nargs=2, action='append') + + parser.add_argument('--config', help='configuration file', nargs = '+') + parser.add_argument('--config-dir', help='directory to search for configuration files', nargs = '+') + + parser.add_argument('--identity', help='Identity to use for the process', required = True, type = str) + + parser.add_argument('--key-dir', help='Directories to search for key files', nargs='+') + parser.add_argument('--data-dir', help='Path for storing generated files', type=str) + + parser.add_argument('--logfile', help='Name of the log file, __screen__ for standard output', type=str) + parser.add_argument('--loglevel', help='Logging level', type=str) + + parser.add_argument('--http', help='Port on which to run the http server', type=int) + parser.add_argument('--block-store', help='Name of the file where blocks are stored', type=str) + + parser.add_argument('--test', help='Test for guardian service', action='store_true') + + options = parser.parse_args() + + # first process the options necessary to load the default configuration + if options.config : + conffiles = options.config + + if options.config_dir : + confpaths = options.config_dir + + config_map['identity'] = options.identity + if options.data_dir : + config_map['data'] = options.data_dir + + # set up the configuration mapping from the parameters + if options.bind : + for (k, v) in options.bind : config_map[k] = v + + # parse the configuration files + try : + config = pconfig.parse_configuration_files(conffiles, confpaths, config_map) + except pconfig.ConfigurationException as e : + logger.error(str(e)) + sys.exit(-1) + + # set up the logging configuration + if config.get('Logging') is None : + config['Logging'] = { + 'LogFile' : '__screen__', + 'LogLevel' : 'INFO' + } + if options.logfile : + config['Logging']['LogFile'] = options.logfile + if options.loglevel : + config['Logging']['LogLevel'] = options.loglevel.upper() + + # make the configuration available to all of the PDO modules + pconfig.initialize_shared_configuration(config) + + plogger.setup_loggers(config.get('Logging', {})) + sys.stdout = plogger.stream_to_logger(logging.getLogger('STDOUT'), logging.DEBUG) + sys.stderr = plogger.stream_to_logger(logging.getLogger('STDERR'), logging.WARN) + + # set up the key search paths + if config.get('Key') is None : + config['Key'] = { + 'SearchPath' : [ '.', './keys', config_map['keys'] ], + 'FileName' : options.identity + ".pem" + } + if options.key_dir : + config['Key']['SearchPath'] = options.key_dir + + # set up the enclave service configuration + if config.get('GuardianService') is None : + config['GuardianService'] = { + 'HttpPort' : 7101, + 'Host' : 'localhost', + 'Identity' : options.identity, + } + if options.http : + config['GuardianService']['HttpPort'] = options.http + + # GO! + if options.test : + TestService(config) + else : + LocalMain(config) + +## ----------------------------------------------------------------- +## Entry points +## ----------------------------------------------------------------- +Main() diff --git a/medperf-contract/pdo/medperf/scripts/scripts.py b/medperf-contract/pdo/medperf/scripts/scripts.py new file mode 100644 index 0000000..fd26293 --- /dev/null +++ b/medperf-contract/pdo/medperf/scripts/scripts.py @@ -0,0 +1,31 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from pdo.client.builder.shell import run_shell_command + +import warnings +warnings.catch_warnings() +warnings.simplefilter("ignore") + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def medperf_token() : + run_shell_command('do_medperf_token', 'pdo.medperf.plugins.medperf_token_object') + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def medperf_guardian() : + run_shell_command('do_medperf_guardian', 'pdo.medperf.plugins.medperf_guardian') + diff --git a/medperf-contract/pdo/medperf/wsgi/__init__.py b/medperf-contract/pdo/medperf/wsgi/__init__.py new file mode 100644 index 0000000..ba6acd3 --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/__init__.py @@ -0,0 +1,36 @@ +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pdo.medperf.wsgi.add_endpoint import AddEndpointApp +from pdo.medperf.wsgi.info import InfoApp +from pdo.medperf.wsgi.process_capability import ProcessCapabilityApp +from pdo.medperf.wsgi.provision_token_issuer import ProvisionTokenIssuerApp +from pdo.medperf.wsgi.provision_token_object import ProvisionTokenObjectApp + + +__all__ = [ + 'AddEndpointApp', + 'InfoApp', + 'ProcessCapabilityApp', + 'ProvisionTokenIssuerApp', + 'ProvisionTokenObjectApp' + ] + +wsgi_operation_map = { + 'add_endpoint' : AddEndpointApp, + 'info' : InfoApp, + 'process_capability' : ProcessCapabilityApp, + 'provision_token_issuer' : ProvisionTokenIssuerApp, + 'provision_token_object' : ProvisionTokenObjectApp + } diff --git a/medperf-contract/pdo/medperf/wsgi/add_endpoint.py b/medperf-contract/pdo/medperf/wsgi/add_endpoint.py new file mode 100644 index 0000000..f970ade --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/add_endpoint.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +from http import HTTPStatus +import io +import json + +from pdo.medperf.common.utility import ValidateJSON +from pdo.common.wsgi import ErrorResponse, UnpackJSONRequest + +import logging +logger = logging.getLogger(__name__) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class AddEndpointApp(object) : + + __input_schema__ = { + "type" : "object", + "properties" : { + "contract_id" : {"type" : "string"}, + "ledger_attestation" : { + "type" : "object", + "properties": { + "contract_code_hash": {"type" : "string"}, + "metadata_hash": {"type" : "string"}, + "signature": {"type" : "string"}, + }, + }, + "contract_metadata" : { + "type" : "object", + "properties": { + "verifying_key" : {"type" : "string"}, + "encryption_key" : {"type" : "string"}, + } + }, + "contract_code_metadata" : { + "type" : "object", + "properties" : { + "code_hash": {"type" : "string"}, + "code_nonce": {"type" : "string"}, + }, + }, + }, + } + + # ----------------------------------------------------------------- + def __init__(self, config, capability_store, endpoint_registry) : + self.capability_store = capability_store + self.endpoint_registry = endpoint_registry + self.code_hash = config.get("TokenIssuer", {}).get("CodeHash", "") + self.contractIDs = config.get("TokenIssuer", {}).get("ContractIDs", []) + self.ledger_key = config.get("TokenIssuer", {}).get("LedgerKey", "") + + # ----------------------------------------------------------------- + def __call__(self, environ, start_response) : + try : + request = UnpackJSONRequest(environ) + if not ValidateJSON(request, self.__input_schema__) : + return ErrorResponse(start_response, "invalid JSON") + + contract_id = request['contract_id'] + ledger_attestation = request['ledger_attestation'] + contract_metadata = request['contract_metadata'] + contract_code_metadata = request['contract_code_metadata'] + + except KeyError as ke : + logger.error('missing field in request: %s', ke) + return ErrorResponse(start_response, 'missing field {0}'.format(ke)) + except Exception as e : + logger.error("unknown exception unpacking request (Invoke); %s", str(e)) + return ErrorResponse(start_response, "unknown exception while unpacking request") + + # verify the endpoint information + ## ## + + ## what is the policy for allowing an endpoint to be registered? + if self.code_hash and contract_code_metadata["code_hash"] != self.code_hash : + return ErrorResponse(start_response, 'endpoint not authorized') + if self.contractIDs and contract_id not in self.contractIDs : + return ErrorResponse(start_response, 'endpoint not authorized') + + # add the endpoint to the endpoint registry + verifying_key = contract_metadata['verifying_key'] + encryption_key = contract_metadata['encryption_key'] + self.endpoint_registry.set_endpoint(contract_id, verifying_key, encryption_key) + + # return success + result = json.dumps({'success' : True}).encode() + status = "{0} {1}".format(HTTPStatus.OK.value, HTTPStatus.OK.name) + headers = [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(result))) + ] + start_response(status, headers) + return [result] diff --git a/medperf-contract/pdo/medperf/wsgi/info.py b/medperf-contract/pdo/medperf/wsgi/info.py new file mode 100644 index 0000000..96c3eee --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/info.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This file defines the InfoApp class, a WSGI interface class for +handling requests for enclave service information. +""" + +import json + +from http import HTTPStatus +from pdo.common.wsgi import ErrorResponse + +import logging +logger = logging.getLogger(__name__) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class InfoApp(object) : + def __init__(self, config, capability_store, endpoint_registry) : + self.storage_url = config['StorageService']['URL'] + self.capability_store = capability_store + self.endpoint_registry = endpoint_registry + + def __call__(self, environ, start_response) : + try : + response = dict() + response['verifying_key'] = self.capability_store.svc_capability_key.verifying_key + response['encryption_key'] = self.capability_store.svc_capability_key.encryption_key + response['storage_service_url'] = self.storage_url + + result = json.dumps(response).encode() + except Exception as e : + logger.exception("info") + return ErrorResponse(start_response, "exception; {0}".format(str(e))) + + status = "{0} {1}".format(HTTPStatus.OK.value, HTTPStatus.OK.name) + headers = [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(result))) + ] + start_response(status, headers) + return [result] diff --git a/medperf-contract/pdo/medperf/wsgi/process_capability.py b/medperf-contract/pdo/medperf/wsgi/process_capability.py new file mode 100644 index 0000000..9451813 --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/process_capability.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +from http import HTTPStatus +import json + +from pdo.medperf.common.utility import ValidateJSON +from pdo.medperf.common.secrets import recv_secret +from pdo.medperf.operations import capability_handler_map +from pdo.common.wsgi import ErrorResponse, UnpackJSONRequest + +import logging +logger = logging.getLogger(__name__) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class ProcessCapabilityApp(object) : + __input_schema__ = { + "type" : "object", + "properties" : { + "minted_identity" : { "type" : "string" }, + "operation" : { + "type" : "object", + "properties" : { + "encrypted_session_key" : { "type" : "string" }, + "session_key_iv" : { "type" : "string" }, + "encrypted_message" : { "type" : "string" }, + }, + }, + } + } + + __operation_schema__ = { + "type" : "object", + "properties" : { + "nonce" : { "type" : "string" }, + "method_name" : { "type" : "string" }, + "parameters" : { "type" : "object" }, + } + } + + # ----------------------------------------------------------------- + def __init__(self, config, capability_store, endpoint_registry) : + self.config = config + self.capability_store = capability_store + self.endpoint_registry = endpoint_registry + + self.capability_handler_map = {} + for (op, handler) in capability_handler_map.items() : + self.capability_handler_map[op] = handler(config) + + # ----------------------------------------------------------------- + def __call__(self, environ, start_response) : + # unpack the request, this is WSGI magic + try : + request = UnpackJSONRequest(environ) + if not ValidateJSON(request, self.__input_schema__) : + return ErrorResponse(start_response, "invalid JSON") + + capability_key = self.capability_store.get_capability_key(request['minted_identity']) + + operation_message = recv_secret(capability_key, request['operation']) + if not ValidateJSON(operation_message, self.__operation_schema__) : + return ErrorResponse(start_response, "invalid JSON") + + except KeyError as ke : + logger.error('missing field in request: %s', ke) + return ErrorResponse(start_response, 'missing field {0}'.format(ke)) + except Exception as e : + logger.error("unknown exception unpacking request (ProcessCapability); %s", str(e)) + return ErrorResponse(start_response, "unknown exception while unpacking request") + + # dispatch the operation + try : + method_name = operation_message['method_name'] + parameters = operation_message['parameters'] + logger.info("process capability operation %s with parameters %s", method_name, parameters) + + operation = self.capability_handler_map[method_name] + operation_result = operation(parameters) + if operation_result is None : + return ErrorResponse(start_response, "operation failed") + + except KeyError as ke : + logger.error('unknown operation: %s', ) + return ErrorResponse(start_response, 'missing field {0}'.format(ke)) + except Exception as e : + logger.error("unknown exception performing operation (ProcessCapability); %s", str(e)) + return ErrorResponse(start_response, "unknown exception while performing operation") + + # and process the result + result = bytes(json.dumps(operation_result), 'utf8') + status = "{0} {1}".format(HTTPStatus.OK.value, HTTPStatus.OK.name) + headers = [ + ('Content-Type', 'application/octet-stream'), + ('Content-Transfer-Encoding', 'utf-8'), + ('Content-Length', str(len(result))) + ] + start_response(status, headers) + return [result] diff --git a/medperf-contract/pdo/medperf/wsgi/provision_token_issuer.py b/medperf-contract/pdo/medperf/wsgi/provision_token_issuer.py new file mode 100644 index 0000000..14b7dd0 --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/provision_token_issuer.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +from http import HTTPStatus +import json + +from pdo.medperf.common.utility import ValidateJSON +from pdo.medperf.common.secrets import send_secret +from pdo.common.wsgi import ErrorResponse, UnpackJSONRequest +import pdo.common.crypto as crypto + +import logging +logger = logging.getLogger(__name__) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class ProvisionTokenIssuerApp(object) : + __input_schema__ = { + "type" : "object", + "properties" : { + "contract_id" : {"type" : "string"}, + } + } + + # ----------------------------------------------------------------- + def __init__(self, config, capability_store, endpoint_registry) : + self.capability_store = capability_store + self.endpoint_registry = endpoint_registry + + # ----------------------------------------------------------------- + def __call__(self, environ, start_response) : + # unpack the request, this is WSGI magic + try : + request = UnpackJSONRequest(environ) + if not ValidateJSON(request, self.__input_schema__) : + return ErrorResponse(start_response, "invalid JSON") + + contract_id = request['contract_id'] + issuer_keys = self.endpoint_registry.get_endpoint(contract_id) + + except Exception as e : + logger.error("unknown exception unpacking request (Invoke); %s", str(e)) + return ErrorResponse(start_response, "unknown exception while unpacking request") + + # get the keys + provisioning_secret = dict() + provisioning_secret['capability_management_key'] = self.capability_store.mgmt_capability_key.encryption_key + provisioning_package = send_secret(issuer_keys, provisioning_secret) + + # return success + result = json.dumps(provisioning_package).encode() + status = "{0} {1}".format(HTTPStatus.OK.value, HTTPStatus.OK.name) + headers = [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(result))) + ] + start_response(status, headers) + return [result] diff --git a/medperf-contract/pdo/medperf/wsgi/provision_token_object.py b/medperf-contract/pdo/medperf/wsgi/provision_token_object.py new file mode 100644 index 0000000..4324e7a --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/provision_token_object.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +from http import HTTPStatus +import json + +from pdo.medperf.common.utility import ValidateJSON +from pdo.medperf.common.secrets import send_secret, recv_secret +from pdo.common.wsgi import ErrorResponse, UnpackJSONRequest +from pdo.common.keys import EnclaveKeys + +import logging +logger = logging.getLogger(__name__) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class ProvisionTokenObjectApp(object) : + __secret_schema__ = { + "type" : "object", + "properties" : { + "minted_identity" : { "type" : "string" }, + "token_description" : { "type" : "string" }, + "token_object_encryption_key" : { "type" : "string" }, + "token_object_verifying_key" : { "type" : "string" }, + "token_metadata": {"type" : "object" }, + }, + } + + # ----------------------------------------------------------------- + def __init__(self, config, capability_store, endpoint_registry) : + self.capability_store = capability_store + self.endpoint_registry = endpoint_registry + + # ----------------------------------------------------------------- + def __call__(self, environ, start_response) : + # unpack the request, this is WSGI magic + try : + request = UnpackJSONRequest(environ) + # the incoming message is in standard secret format, the session key + # should be encrypted with the management capability key + secret_message = recv_secret(self.capability_store.mgmt_capability_key, request) + + if not ValidateJSON(secret_message, self.__secret_schema__) : + return ErrorResponse(start_response, "invalid JSON") + + except KeyError as ke : + logger.error('missing field in request: %s', ke) + return ErrorResponse(start_response, 'missing field {0}'.format(ke)) + except Exception as e : + logger.error("unknown exception unpacking request (Invoke); %s", str(e)) + return ErrorResponse(start_response, "unknown exception while unpacking request") + + # create and save the token object capability key + capability_key = self.capability_store.create_capability_key(secret_message['minted_identity']) + token_object_package = dict() + token_object_package['minted_identity'] = secret_message['minted_identity'] + token_object_package['token_description'] = secret_message['token_description'] + token_object_package['token_metadata'] = secret_message['token_metadata'] + token_object_package['capability_generation_key'] = capability_key.encryption_key + + + # the provisioning package for the token object is encrypted with the + # token object's encryption key + token_object_keys = EnclaveKeys( + secret_message['token_object_verifying_key'], + secret_message['token_object_encryption_key']) + secret_package = send_secret(token_object_keys, token_object_package) + + # create the result + result = bytes(json.dumps(secret_package), 'utf8') + status = "{0} {1}".format(HTTPStatus.OK.value, HTTPStatus.OK.name) + headers = [ + ('Content-Type', 'application/octet-stream'), + ('Content-Transfer-Encoding', 'utf-8'), + ('Content-Length', str(len(result))) + ] + start_response(status, headers) + return [result] diff --git a/medperf-contract/scripts/gs_start.sh b/medperf-contract/scripts/gs_start.sh new file mode 100755 index 0000000..580e5d3 --- /dev/null +++ b/medperf-contract/scripts/gs_start.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=medperf_guardian_service +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/${F_SERVICE_NAME} + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +F_SCRIPT_NAME=$(basename ${BASH_SOURCE[-1]} ) + +F_CLEAN='no' +F_OUTPUTDIR='' + +F_USAGE='-c|--clean -o|--output dir --help -- ' +F_SHORT_OPTS='co:' +F_LONG_OPTS='clean,output:,help' + + +TEMP=$(getopt -o ${F_SHORT_OPTS} --long ${F_LONG_OPTS} -n "${F_SCRIPT_NAME}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT_NAME} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -c|--clean) F_CLEAN="yes" ; shift 1 ;; + -o|--output) F_OUTPUTDIR="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +# do not start if the service is already running +PLIST=$(pgrep -f ${F_SERVICE_CMD}) +if [ -n "$PLIST" ] ; then + echo existing services detected, please shutdown first + exit 1 +fi + +# start service asynchronously +yell start ${F_SERVICE_NAME} + +if [ "${F_CLEAN}" == "yes" ]; then + rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.log +fi + +rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid + +if [ "${F_OUTPUTDIR}" != "" ] ; then + EFILE="${F_OUTPUTDIR}/${F_SERVICE_NAME}.err" + OFILE="${F_OUTPUTDIR}/${F_SERVICE_NAME}.out" + rm -f $EFILE $OFILE +else + EFILE=/dev/null + OFILE=/dev/null +fi + +${F_SERVICE_CMD} $@ 2> $EFILE > $OFILE & +echo $! > ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid + +# verify that the service has started before continuing, we'll +# wait for 10 seconds plus or minus +declare tries=0 +declare max_tries=10 + +until $(${F_SERVICE_CMD} $@ --test 2> /dev/null > /dev/null) ; do + sleep 1 + tries=$((tries+1)) + if [ $tries = $max_tries ] ; then + die guardian service failed to start + fi +done diff --git a/medperf-contract/scripts/gs_status.sh b/medperf-contract/scripts/gs_status.sh new file mode 100755 index 0000000..7d2fcbc --- /dev/null +++ b/medperf-contract/scripts/gs_status.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=medperf_guardian_service +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/${F_SERVICE_NAME} + +if [ -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid ]; then + F_PID=$(cat ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid) +else + yell unable to locate service pid file + + F_PID=$(pgrep -f "${F_SERVICE_CMD}") + if [ ! -n "${F_PID}" ] ; then + yell unable to locate service process + exit + fi +fi + +try ps -h --format pid,start,cmd -p $F_PID diff --git a/medperf-contract/scripts/gs_stop.sh b/medperf-contract/scripts/gs_stop.sh new file mode 100755 index 0000000..e22160c --- /dev/null +++ b/medperf-contract/scripts/gs_stop.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=medperf_guardian_service +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/${F_SERVICE_NAME} + +if [ -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid ]; then + F_PID=$(cat ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid) +else + yell unable to locate service pid file + + F_PID=$(pgrep -f ${F_SERVICE_CMD}) + if [ ! -n "${F_PID}" ] ; then + yell unable to locate service process + exit + fi +fi + +# kill the service +if kill -SIGTERM ${F_PID} > /dev/null +then + sleep 1 +fi + +# clean up the PID file if we were successful +if ps -p ${F_PID} > /dev/null +then + yell failed to terminate process ${F_PID} +else + rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid +fi diff --git a/medperf-contract/scripts/ss_start.sh b/medperf-contract/scripts/ss_start.sh new file mode 100755 index 0000000..415e0bb --- /dev/null +++ b/medperf-contract/scripts/ss_start.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=guardian_sservice +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/sservice + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +F_SCRIPT_NAME=$(basename ${BASH_SOURCE[-1]} ) + +F_CLEAN='no' +F_OUTPUTDIR='' + +F_USAGE='-c|--clean -o|--output dir --help -- ' +F_SHORT_OPTS='co:' +F_LONG_OPTS='clean,output:,help' + +TEMP=$(getopt -o ${F_SHORT_OPTS} --long ${F_LONG_OPTS} -n "${F_SCRIPT_NAME}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT_NAME} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -c|--clean) F_CLEAN="yes" ; shift 1 ;; + -o|--output) F_OUTPUTDIR="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +# do not start if the service is already running +PLIST=$(pgrep -f ${F_SERVICE_CMD}) +if [ -n "$PLIST" ] ; then + echo existing storage service detected, please shutdown first + exit 1 +fi + +# start service asynchronously +echo start ${F_SERVICE_NAME} + +if [ "${F_CLEAN}" == "yes" ]; then + rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.log + rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.log +fi + +rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid + +if [ "${F_OUTPUTDIR}" != "" ] ; then + EFILE="${F_OUTPUTDIR}/${F_SERVICE_NAME}.err" + OFILE="${F_OUTPUTDIR}/${F_SERVICE_NAME}.out" + rm -f $EFILE $OFILE +else + EFILE=/dev/null + OFILE=/dev/null +fi + +${F_SERVICE_CMD} $@ 2> $EFILE > $OFILE & +echo $! > ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid diff --git a/medperf-contract/scripts/ss_status.sh b/medperf-contract/scripts/ss_status.sh new file mode 100755 index 0000000..0f6f13e --- /dev/null +++ b/medperf-contract/scripts/ss_status.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=guardian_sservice +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/${F_SERVICE_NAME} + +if [ -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid ]; then + F_PID=$(cat ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid) +else + yell unable to locate service pid file + + F_PID=$(pgrep -f "${F_SERVICE_CMD}") + if [ ! -n "${F_PID}" ] ; then + yell unable to locate service process + exit + fi +fi + +try ps -h --format pid,start,cmd -p $F_PID diff --git a/medperf-contract/scripts/ss_stop.sh b/medperf-contract/scripts/ss_stop.sh new file mode 100755 index 0000000..5732aa1 --- /dev/null +++ b/medperf-contract/scripts/ss_stop.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=guardian_sservice +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/sservice + +if [ -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid ]; then + F_PID=$(cat ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid) +else + yell unable to locate service pid file + + F_PID=$(pgrep -f ${F_SERVICE_CMD}) + if [ ! -n "${F_PID}" ] ; then + yell unable to locate service process + exit + fi +fi + +# kill the service +if kill -SIGTERM ${F_PID} > /dev/null +then + sleep 1 +fi + +# clean up the PID file if we were successful +if ps -p ${F_PID} > /dev/null +then + yell failed to terminate process ${F_PID} +else + rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid +fi diff --git a/medperf-contract/setup.py b/medperf-contract/setup.py new file mode 100644 index 0000000..9f84d93 --- /dev/null +++ b/medperf-contract/setup.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import subprocess +import warnings + +# this should only be run with python3 +import sys +if sys.version_info[0] < 3: + print('ERROR: must run with python3') + sys.exit(1) + +from setuptools import setup, find_packages, find_namespace_packages + +# ----------------------------------------------------------------- +# Versions are tied to tags on the repository; to compute correctly +# it is necessary to be within the repository itself hence the need +# to set the cwd for the bin/get_version command. +# ----------------------------------------------------------------- +root_dir = os.path.dirname(os.path.realpath(__file__)) +try : + pdo_contracts_version = subprocess.check_output( + 'bin/get_version', cwd=os.path.join(root_dir, os.pardir)).decode('ascii').strip() +except Exception as e : + warnings.warn('Failed to get pdo_contracts version, using the default') + pdo_contracts_version = '0.0.0' + +try : + pdo_client_version = subprocess.check_output( + 'bin/get_version', cwd=os.path.join(root_dir, os.pardir, 'private-data-objects')).decode('ascii').strip() +except Exception as e : + warnings.warn('Failed to get pdo_client version, using the default') + pdo_client_version = '0.0.0' + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +setup( + name='pdo_medperf', + version=pdo_contracts_version, + description='Implementation of guardian service for MedPerf experiment execution', + author='Wenyi Tang, Intel Labs', + author_email='wenyi1.tang@intel.com', + url='http://www.intel.com', + package_dir = { + 'pdo' : 'pdo', + 'pdo.medperf.resources.etc' : 'etc', + 'pdo.medperf.resources.context' : 'context', + 'pdo.medperf.resources.contracts' : '../build/medperf-contract', + 'pdo.medperf.resources.scripts' : 'scripts', + }, + packages = [ + 'pdo', + 'pdo.medperf', + 'pdo.medperf.plugins', + 'pdo.medperf.scripts', + 'pdo.medperf.common', + 'pdo.medperf.operations', + 'pdo.medperf.wsgi', + 'pdo.medperf.resources', + 'pdo.medperf.resources.etc', + 'pdo.medperf.resources.context', + 'pdo.medperf.resources.contracts', + 'pdo.medperf.resources.scripts', + ], + include_package_data=True, + # add everything from requirements.txt here + install_requires = [ + 'colorama', + 'colorlog', + 'lmdb', + 'toml', + 'twisted', + 'jsonschema>=3.0.1', + 'requests', + 'urllib3', + 'pdo-client>=' + pdo_client_version, + 'pdo-common-library>=' + pdo_client_version, + 'pdo-sservice>=' + pdo_client_version, + 'pdo-exchange>=' + pdo_contracts_version, + ], + entry_points = { + 'console_scripts' : [ + 'medperf_token=pdo.medperf.scripts.scripts:medperf_token', + 'medperf_guardian_service=pdo.medperf.scripts.guardianCLI:Main', + ] + } +) diff --git a/medperf-contract/test/.gitignore b/medperf-contract/test/.gitignore new file mode 100644 index 0000000..eb0abc5 --- /dev/null +++ b/medperf-contract/test/.gitignore @@ -0,0 +1 @@ +test_context.toml diff --git a/medperf-contract/test/mp_test.sh b/medperf-contract/test/mp_test.sh new file mode 100755 index 0000000..e16b5fa --- /dev/null +++ b/medperf-contract/test/mp_test.sh @@ -0,0 +1,410 @@ +#!/bin/bash + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +: "${PDO_LEDGER_URL?Missing environment variable PDO_LEDGER_URL}" +: "${PDO_HOME?Missing environment variable PDO_HOME}" +: "${PDO_SOURCE_ROOT?Missing environment variable PDO_SOURCE_ROOT}" +: "${MEDPERF_SQLITE_PATH?Missing environment variable MEDPERF_SQLITE_PATH}" +: "${MEDPERF_VENV_PATH?Missing environment variable MEDPERF_VENV_PATH}" +: "${MEDPERF_HOME?Missing environment variable MEDPERF_HOME}" +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +source ${PDO_HOME}/bin/lib/common.sh +check_python_version + +# note +# ./script_test.sh --host 10.54.66.43 --ledger http://10.54.66.43:6600 + +if ! command -v pdo-shell &> /dev/null ; then + die unable to locate pdo-shell +fi + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +if [ "${PDO_LEDGER_TYPE}" == "ccf" ]; then + if [ ! -f "${PDO_LEDGER_KEY_ROOT}/networkcert.pem" ]; then + die "CCF ledger keys are missing, please copy and try again" + fi +fi + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +SCRIPTDIR="$(dirname $(readlink --canonicalize ${BASH_SOURCE}))" +SOURCE_ROOT="$(realpath ${SCRIPTDIR}/..)" + +F_SCRIPT=$(basename ${BASH_SOURCE[-1]} ) +F_SERVICE_HOST=${PDO_HOSTNAME} +F_GUARDIAN_HOST=10.54.66.42 +F_LEDGER_URL=${PDO_LEDGER_URL} +F_LOGLEVEL=${PDO_LOG_LEVEL:-info} +F_LOGFILE=${PDO_LOG_FILE:-__screen__} +F_CONTEXT_FILE=${SOURCE_ROOT}/test/test_context.toml +F_CONTEXT_TEMPLATES=${PDO_HOME}/contracts/medperf/context +F_EXCHANGE_TEMPLATES=${PDO_HOME}/contracts/exchange/context + +F_USAGE='--host service-host | --ledger url | --loglevel [debug|info|warn] | --logfile file' +SHORT_OPTS='h:l:' +LONG_OPTS='host:,ledger:,loglevel:,logfile:' + +TEMP=$(getopt -o ${SHORT_OPTS} --long ${LONG_OPTS} -n "${F_SCRIPT}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -h|--host) F_SERVICE_HOST="$2" ; shift 2 ;; + -1|--ledger) F_LEDGER_URL="$2" ; shift 2 ;; + --loglevel) F_LOGLEVEL="$2" ; shift 2 ;; + --logfile) F_LOGFILE="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +F_SERVICE_SITE_FILE=${PDO_HOME}/etc/sites/${F_SERVICE_HOST}.toml +if [ ! -f ${F_SERVICE_SITE_FILE} ] ; then + die unable to locate the service information file ${F_SERVICE_SITE_FILE}; \ + please copy the site.toml file from the service host +fi + +F_SERVICE_GROUPS_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_groups_db +F_SERVICE_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_db + +_COMMON_=("--logfile ${F_LOGFILE}" "--loglevel ${F_LOGLEVEL}") +_COMMON_+=("--ledger ${F_LEDGER_URL}") +_COMMON_+=("--groups-db ${F_SERVICE_GROUPS_DB_FILE}") +_COMMON_+=("--service-db ${F_SERVICE_DB_FILE}") +SHORT_OPTS=${_COMMON_[@]} + +_COMMON_+=("--context-file ${F_CONTEXT_FILE}") +OPTS=${_COMMON_[@]} + +# ----------------------------------------------------------------- +# Make sure the keys and eservice database are created and up to date +# ----------------------------------------------------------------- +F_KEY_FILES=() +KEYGEN=${PDO_SOURCE_ROOT}/build/__tools__/make-keys +if [ ! -f ${PDO_HOME}/keys/red_type_private.pem ]; then + yell create keys for the contracts + for color in red green blue orange purple white ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_{type,vetting,issuer}_{private,public}.pem) + done + + for color in green1 green2 green3; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_issuer_{private,public}.pem) + done + + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_{type,vetting,issuer}_{private,public}.pem) + for count in 1 2 3 4 5 ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_holder${count} --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_holder${count}_{private,public}.pem) + done +fi + +if [ ! -f ${PDO_HOME}/keys/guardian_service.pem ]; then + yell create keys for the guardian service + ${KEYGEN} --keyfile ${PDO_HOME}/keys/guardian_service --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/guardian_service.pem) + + ${KEYGEN} --keyfile ${PDO_HOME}/keys/guardian_sservice --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/guardian_sservice.pem) +fi + +# ----------------------------------------------------------------- +function cleanup { + rm -f ${F_SERVICE_GROUPS_DB_FILE} ${F_SERVICE_GROUPS_DB_FILE}-lock + rm -f ${F_SERVICE_DB_FILE} ${F_SERVICE_DB_FILE}-lock + rm -f ${F_CONTEXT_FILE} + for key_file in ${F_KEY_FILES[@]} ; do + rm -f ${key_file} + done + + yell "shutdown guardian and storage service" + ${PDO_HOME}/contracts/medperf/scripts/gs_stop.sh + ${PDO_HOME}/contracts/medperf/scripts/ss_stop.sh +} + +trap cleanup EXIT + +# ----------------------------------------------------------------- +# Start the guardian service and the storage service +# ----------------------------------------------------------------- +try ${PDO_HOME}/contracts/medperf/scripts/ss_start.sh -c -o ${PDO_HOME}/logs -- \ + --loglevel debug \ + --config guardian_service.toml \ + --config-dir ${PDO_HOME}/etc/contracts \ + --identity guardian_sservice + +sleep 3 + +try ${PDO_HOME}/contracts/medperf/scripts/gs_start.sh -c -o ${PDO_HOME}/logs -- \ + --loglevel debug \ + --config guardian_service.toml \ + --config-dir ${PDO_HOME}/etc/contracts \ + --identity guardian_service \ + --bind host ${F_GUARDIAN_HOST} \ + --bind service_host ${F_SERVICE_HOST} + +# ----------------------------------------------------------------- +# create the service and groups databases from a site file; the site +# file is assumed to exist in ${PDO_HOME}/etc/sites/${SERVICE_HOST}.toml +# +# by default, the groups will include all available services from the +# service host +# ----------------------------------------------------------------- +yell create the service and groups database for host ${F_SERVICE_HOST} +try pdo-service-db import ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} +try pdo-eservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-pservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-sservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default \ + --replicas 1 --duration 60 + +# ----------------------------------------------------------------- +# setup the contexts that will be used later for the tests, check this +# ----------------------------------------------------------------- +cd "${SOURCE_ROOT}" + +rm -f ${CONTEXT_FILE} + +try pdo-context load ${OPTS} --import-file ${F_CONTEXT_TEMPLATES}/tokens.toml \ + --bind token test1 --bind url http://${F_GUARDIAN_HOST}:7900 + + +yell =========================================================================== +yell Start the tests for PDO and MedPerf +yell =========================================================================== + +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +RED='\033[0;31m' +RESET='\033[0m' + +read -p "Press enter to continue >>" + +echo -e "${GREEN}===========================${RESET}" +echo -e "${GREEN}=== Preparing MedPerf ===${RESET}" +echo -e "${GREEN}===========================${RESET}" + +deactivate + +source $MEDPERF_VENV_PATH'/bin/activate' +cd ${MEDPERF_HOME} +rm -fr medperf_tutorial +cd server +sh reset_db.sh +rm -fr ~/.medperf/localhost_8000 +python seed.py --demo data +cd .. +sh tutorials_scripts/setup_data_tutorial.sh + +medperf dataset submit \ + --name "mytestdata" \ + --description "A tutorial dataset" \ + --location "My machine" \ + --data_path "medperf_tutorial/sample_raw_data/images" \ + --labels_path "medperf_tutorial/sample_raw_data/labels" \ + --benchmark 1 + + + +medperf dataset prepare --data_uid 1 +echo -e "${GREEN}Putting some dummy models assosciated with experiment..." +read -p "Press enter to continue >>" +python record_writer/fake_model.py +# clear + +echo -e "${YELLOW}======================================================${RESET}" +echo -e "${YELLOW}The next commmand will set the dataset as operational.${RESET}" +echo -e "${YELLOW}This is a good time to mint a token for the dataset.${RESET}" +echo -e "${YELLOW}======================================================${RESET}" +read -p "Press enter to continue >>" + +medperf dataset set_operational --data_uid 1 + +deactivate +source ${PDO_INSTALL_ROOT}'/bin/activate' + +# mint token here +yell create a token issuer and mint the tokens. This token does not have any policy +try ex_token_issuer create ${OPTS} --contract token.test1.token_issuer +try medperf_token mint_dataset_tokens ${OPTS} --contract token.test1.token_object \ + --dataset_id "mytestdata" + + +deactivate +source $MEDPERF_VENV_PATH'/bin/activate' + +clear + +echo -e "${YELLOW}=======================================================================${RESET}" +echo -e "${YELLOW}The next command will associate the dataset with experiment.${RESET}" +echo -e "${YELLOW}This is a good time to update the token policy for the dataset.${RESET}" +echo -e "${YELLOW}The experiment is associated with 10 models marked from 1-10 ${RESET}" +echo -e "${YELLOW}The policy specifies the dataowner only allows 3 models to be evaluated${RESET}" +echo -e "${YELLOW}========================================================================${RESET}" +read -p "Press enter to continue >>" + +medperf dataset associate --benchmark_uid 1 --data_uid 1 > /dev/null + +deactivate +source ${PDO_INSTALL_ROOT}'/bin/activate' + +# update token policy here +yell the owner of the dataset token updates the policy by adding an experiment id and a list of associated models +yell the experiment contains 10 models, but the dataset can be used only 3 times +try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --dataset_id "mytestdata" \ + --experiment_id "chestxray" \ + --associated_model_ids "10" \ + --max_use_count 3 + +# check token policy here +# yell get dataset and policy info from the token object +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + +# clear + +echo -e "${YELLOW}============================================================${RESET}" +echo -e "${YELLOW}The dataowner can transfer the token to experiment committee${RESET}" +echo -e "${YELLOW}============================================================${RESET}" +read -p "Press enter to continue >>" + +# transfer token here +yell the owner transfers the token to token_holder1 '(experiment committee)' +try medperf_token transfer ${OPTS} --contract token.test1.token_object.token_1 \ + --new-owner token_holder1 + +# echo -e "${YELLOW} Update policy is not allowed from the experiment committee${RESET}" +# update token policy here +yell the experiment committee tries to update the policy to a maximum 10 uese, but fails +medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --experiment_id "chestxray" \ + --dataset_id "mytestdata" \ + --associated_model_ids "10" \ + --max_use_count 10 \ + --identity token_holder1 + +# echo -e "${YELLOW} Update policy is not allowed from the experiment committee${RESET}" +# yell the token_issuer tries to update the policy to a maximum 2 uses +# try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ +# --experiment_id "chestxray" \ +# --dataset_id "mytestdata" \ +# --associated_model_ids "10" \ +# --max_use_count 2 + +# yell Check the new policy +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 + + +# clear +echo -e "${YELLOW}============================================================${RESET}" +echo -e "${YELLOW} Experiment committee can choose which models to use.${RESET}" +echo -e "${YELLOW} These models are marked scheduled in PDO contract. ${RESET}" +echo -e "${YELLOW}============================================================${RESET}" +# read -p "Press enter to continue >>" + +yell Try to use model 3, it works +medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --model_ids_to_evaluate '3' +# out of range not allowed + +# read -p "Press enter to continue >>" +yell Try use a model 15 that is not associated with the experiment, but fails +medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --model_ids_to_evaluate '15' + +# two +# read -p "Press enter to continue >>" +yell Try to use model 4, it works +try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --model_ids_to_evaluate '4' + +yell Try to use model 5, it works +try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --model_ids_to_evaluate '5' + +# three not allowed +# read -p "Press enter to continue >>" +yell Try to use model 6, it fails because the maximum use count is 3 +try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --model_ids_to_evaluate '6' + +# read -p "Press enter to continue >>" +# yell check the token policy status again +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 + +read -p "Press enter to continue >>" +# clear +echo -e "${YELLOW}============================================================${RESET}" +echo -e "${YELLOW}The experiment committee can pack all scheduled models into ${RESET}" +echo -e "${YELLOW}a single experiment workorder send it to the medperf server.${RESET}" +echo -e "${YELLOW}============================================================${RESET}" +read -p "Press enter to continue >>" + +yell Experiment Committee gets a packed workorder and submit to the MedPerf server +medperf_token experiment_order ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --medperf_sqlite_path + + + +yell Check the token policy status again +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 + +read -p "Press enter to check the results >>" + +echo -e "${YELLOW}============================================================${RESET}" +echo -e "${YELLOW}The results of the experiment look like this.${RESET}" +echo -e "${YELLOW}============================================================${RESET}" +# +for i in {3,4,5}; +do + # echo -e "${YELLOW}============================================================${RESET}" + echo -e "${YELLOW}Model $i${RESET}" + # echo -e "${YELLOW}============================================================${RESET}" + cat $MEDPERF_HOME/test_resource/test_results/$i* +done +read -p "Press enter to quit" + +exit + diff --git a/medperf-contract/test/script_test.sh b/medperf-contract/test/script_test.sh new file mode 100755 index 0000000..5e1b3aa --- /dev/null +++ b/medperf-contract/test/script_test.sh @@ -0,0 +1,295 @@ +#!/bin/bash + +# Copyright 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +: "${PDO_LEDGER_URL?Missing environment variable PDO_LEDGER_URL}" +: "${PDO_HOME?Missing environment variable PDO_HOME}" +: "${PDO_SOURCE_ROOT?Missing environment variable PDO_SOURCE_ROOT}" +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +source ${PDO_HOME}/bin/lib/common.sh +check_python_version + +# note +# ./script_test.sh --host 10.54.66.43 --ledger http://10.54.66.43:6600 + +if ! command -v pdo-shell &> /dev/null ; then + die unable to locate pdo-shell +fi + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +if [ "${PDO_LEDGER_TYPE}" == "ccf" ]; then + if [ ! -f "${PDO_LEDGER_KEY_ROOT}/networkcert.pem" ]; then + die "CCF ledger keys are missing, please copy and try again" + fi +fi + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +SCRIPTDIR="$(dirname $(readlink --canonicalize ${BASH_SOURCE}))" +SOURCE_ROOT="$(realpath ${SCRIPTDIR}/..)" + +F_SCRIPT=$(basename ${BASH_SOURCE[-1]} ) +F_SERVICE_HOST=${PDO_HOSTNAME} +F_GUARDIAN_HOST=10.54.66.42 +F_LEDGER_URL=${PDO_LEDGER_URL} +F_LOGLEVEL=${PDO_LOG_LEVEL:-info} +F_LOGFILE=${PDO_LOG_FILE:-__screen__} +F_CONTEXT_FILE=${SOURCE_ROOT}/test/test_context.toml +F_CONTEXT_TEMPLATES=${PDO_HOME}/contracts/medperf/context +F_EXCHANGE_TEMPLATES=${PDO_HOME}/contracts/exchange/context + +F_USAGE='--host service-host | --ledger url | --loglevel [debug|info|warn] | --logfile file' +SHORT_OPTS='h:l:' +LONG_OPTS='host:,ledger:,loglevel:,logfile:' + +TEMP=$(getopt -o ${SHORT_OPTS} --long ${LONG_OPTS} -n "${F_SCRIPT}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -h|--host) F_SERVICE_HOST="$2" ; shift 2 ;; + -1|--ledger) F_LEDGER_URL="$2" ; shift 2 ;; + --loglevel) F_LOGLEVEL="$2" ; shift 2 ;; + --logfile) F_LOGFILE="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +F_SERVICE_SITE_FILE=${PDO_HOME}/etc/sites/${F_SERVICE_HOST}.toml +if [ ! -f ${F_SERVICE_SITE_FILE} ] ; then + die unable to locate the service information file ${F_SERVICE_SITE_FILE}; \ + please copy the site.toml file from the service host +fi + +F_SERVICE_GROUPS_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_groups_db +F_SERVICE_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_db + +_COMMON_=("--logfile ${F_LOGFILE}" "--loglevel ${F_LOGLEVEL}") +_COMMON_+=("--ledger ${F_LEDGER_URL}") +_COMMON_+=("--groups-db ${F_SERVICE_GROUPS_DB_FILE}") +_COMMON_+=("--service-db ${F_SERVICE_DB_FILE}") +SHORT_OPTS=${_COMMON_[@]} + +_COMMON_+=("--context-file ${F_CONTEXT_FILE}") +OPTS=${_COMMON_[@]} + +# ----------------------------------------------------------------- +# Make sure the keys and eservice database are created and up to date +# ----------------------------------------------------------------- +F_KEY_FILES=() +KEYGEN=${PDO_SOURCE_ROOT}/build/__tools__/make-keys +if [ ! -f ${PDO_HOME}/keys/red_type_private.pem ]; then + yell create keys for the contracts + for color in red green blue orange purple white ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_{type,vetting,issuer}_{private,public}.pem) + done + + for color in green1 green2 green3; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_issuer_{private,public}.pem) + done + + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_{type,vetting,issuer}_{private,public}.pem) + for count in 1 2 3 4 5 ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_holder${count} --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_holder${count}_{private,public}.pem) + done +fi + +if [ ! -f ${PDO_HOME}/keys/guardian_service.pem ]; then + yell create keys for the guardian service + ${KEYGEN} --keyfile ${PDO_HOME}/keys/guardian_service --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/guardian_service.pem) + + ${KEYGEN} --keyfile ${PDO_HOME}/keys/guardian_sservice --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/guardian_sservice.pem) +fi + +# ----------------------------------------------------------------- +function cleanup { + rm -f ${F_SERVICE_GROUPS_DB_FILE} ${F_SERVICE_GROUPS_DB_FILE}-lock + rm -f ${F_SERVICE_DB_FILE} ${F_SERVICE_DB_FILE}-lock + rm -f ${F_CONTEXT_FILE} + for key_file in ${F_KEY_FILES[@]} ; do + rm -f ${key_file} + done + + yell "shutdown guardian and storage service" + ${PDO_HOME}/contracts/medperf/scripts/gs_stop.sh + ${PDO_HOME}/contracts/medperf/scripts/ss_stop.sh +} + +trap cleanup EXIT + +# ----------------------------------------------------------------- +# Start the guardian service and the storage service +# ----------------------------------------------------------------- +try ${PDO_HOME}/contracts/medperf/scripts/ss_start.sh -c -o ${PDO_HOME}/logs -- \ + --loglevel debug \ + --config guardian_service.toml \ + --config-dir ${PDO_HOME}/etc/contracts \ + --identity guardian_sservice + +sleep 3 + +try ${PDO_HOME}/contracts/medperf/scripts/gs_start.sh -c -o ${PDO_HOME}/logs -- \ + --loglevel debug \ + --config guardian_service.toml \ + --config-dir ${PDO_HOME}/etc/contracts \ + --identity guardian_service \ + --bind host ${F_GUARDIAN_HOST} \ + --bind service_host ${F_SERVICE_HOST} + +# ----------------------------------------------------------------- +# create the service and groups databases from a site file; the site +# file is assumed to exist in ${PDO_HOME}/etc/sites/${SERVICE_HOST}.toml +# +# by default, the groups will include all available services from the +# service host +# ----------------------------------------------------------------- +yell create the service and groups database for host ${F_SERVICE_HOST} +try pdo-service-db import ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} +try pdo-eservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-pservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-sservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default \ + --replicas 1 --duration 60 + +# ----------------------------------------------------------------- +# setup the contexts that will be used later for the tests, check this +# ----------------------------------------------------------------- +cd "${SOURCE_ROOT}" + +rm -f ${CONTEXT_FILE} + +try pdo-context load ${OPTS} --import-file ${F_CONTEXT_TEMPLATES}/tokens.toml \ + --bind token test1 --bind url http://${F_GUARDIAN_HOST}:7900 + +# ----------------------------------------------------------------- +# start the tests +# ----------------------------------------------------------------- + +yell create a token issuer and mint the tokens. This token does not have any policy +try ex_token_issuer create ${OPTS} --contract token.test1.token_issuer +try medperf_token mint_dataset_tokens ${OPTS} --contract token.test1.token_object \ + --dataset_id "test_dataset_1" + +# yell get dataset and policy info from the token object +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + +yell the owner of the token updates the policy by adding an experiment id and a list of associated models with the experiment id +yell the experiment contains 5 models, but the dataset can be used only 5 times +try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --dataset_id "test_dataset_1" \ + --experiment_id "test_experiment_1" \ + --associated_model_ids "5" \ + --max_use_count 5 + +yell get dataset and policy info from the token object +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + + +yell the owner transfers the token to token_holder1 +try medperf_token transfer ${OPTS} --contract token.test1.token_object.token_1 \ + --new-owner token_holder1 + +yell the token_holder1 tries to update the policy to a maximum 10 uese, but fails +medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --experiment_id "test_experiment_1" \ + --dataset_id "test_dataset_1" \ + --associated_model_ids "10" \ + --max_use_count 10 \ + --identity token_holder1 + +yell the token_issuer tries to update the policy to a maximum 2 uses +try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --experiment_id "test_experiment_1" \ + --dataset_id "test_dataset_1" \ + --associated_model_ids "10" \ + --max_use_count 2 + +yell the token_holder1 tries to check the policy again +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 + + +yell the token_holder1 tries to use the dataset with the first models, this time works +medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "test_dataset_1" \ + --model_ids_to_evaluate '3' + +yell check the policy again +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 + + +yell the token_holder1 tries to use a model that is not associated with the experiment, but fails +medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "test_dataset_1" \ + --model_ids_to_evaluate '15' + +yell the token_holder1 tries to use the dataset with the second model, this time works +try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "test_dataset_1" \ + --model_ids_to_evaluate '5' + +# yell the token_holder1 tries to use the dataset with the third model, this time fails +# try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 \ +# --dataset_id "test_dataset_1" \ +# --model_ids_to_evaluate '4' + +# yell check the policy again +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 + +yell get experiment order and submit to the guardian service +try medperf_token experiment_order ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "test_dataset_1" \ + +yell check the policy again +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 + +# yell a test call of the hello world method in the contract +# try medperf_token owner_test ${OPTS} --contract token.test1.token_object.token_1 + +# yell use the dataset for a model, this is the first usage prior to transfer +# try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 + # --user_inputs '{"inputs": "Can you please let us know more details about yourself ?"}' + +# yell a test call of the hello world method in the contract +# try medperf_token owner_test ${OPTS} --contract token.test1.token_object.token_1 + +read -p "Press enter to quit" + +exit