From edfcb82daf7438a6a3c56ef6420b5638acf402a7 Mon Sep 17 00:00:00 2001 From: Prakash Narayana Moorthy Date: Mon, 8 Jul 2024 03:06:12 +0000 Subject: [PATCH 1/5] Minor change that adds kwargs to the op_initialize client plugin call. Change enables other use cases to re-use the exchange op_initialize method, while passing in use-case specific initiazation arguments via kwargs Signed-off-by: Prakash Narayana Moorthy --- exchange-contract/pdo/exchange/plugins/token_object.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exchange-contract/pdo/exchange/plugins/token_object.py b/exchange-contract/pdo/exchange/plugins/token_object.py index 35ba2e6..c9af97e 100644 --- a/exchange-contract/pdo/exchange/plugins/token_object.py +++ b/exchange-contract/pdo/exchange/plugins/token_object.py @@ -240,7 +240,8 @@ def mint_one_token(cls, state, to_context, ti_context, dg_context, ledger_submit state, to_context, to_session, ledger_key, to_package, - authority) + authority, + **kwargs) return to_save_file @classmethod From 986f4d25165b8e687ae6e269b60704b8f5320564 Mon Sep 17 00:00:00 2001 From: Prakash Narayana Moorthy Date: Mon, 8 Jul 2024 03:13:07 +0000 Subject: [PATCH 2/5] Token object contract related files for tokenizing Hugging Face models. The token object implements the policy for accessing HF hosted models. The token object code inherits several base methods from the exchange contract token object, and only implements asset-use specific methods. In addition, the token object initialization method is used to store asset (HF model) specifc details, so of which are secrets, and others used to provide meta data regarding the asset for a prospective token user Signed-off-by: Prakash Narayana Moorthy --- CMakeLists.txt | 4 +- hfmodels-contract/CMakeLists.txt | 37 ++ hfmodels-contract/contracts/token_object.cpp | 78 +++++ .../hfmodels/contracts/token_object.cpp | 318 ++++++++++++++++++ hfmodels-contract/hfmodels/token_object.h | 91 +++++ hfmodels-contract/hfmodels_common.cmake | 39 +++ inference-contract/inference_common.cmake | 2 +- 7 files changed, 567 insertions(+), 2 deletions(-) create mode 100644 hfmodels-contract/CMakeLists.txt create mode 100644 hfmodels-contract/contracts/token_object.cpp create mode 100644 hfmodels-contract/hfmodels/contracts/token_object.cpp create mode 100644 hfmodels-contract/hfmodels/token_object.h create mode 100644 hfmodels-contract/hfmodels_common.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e08083..06d1552 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,8 @@ PROJECT(pdo-contracts) LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/exchange-contract") LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/digital-asset-contract") +LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/inference-contract") +LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/hfmodels-contract") INCLUDE(ProjectVariables) LIST(APPEND CMAKE_MODULE_PATH "${PDO_SOURCE_ROOT}/contracts/wawaka") @@ -27,7 +29,7 @@ INCLUDE(wawaka_common) LIST(APPEND WASM_LIBRARIES ${WW_COMMON_LIB}) LIST(APPEND WASM_INCLUDES ${WW_COMMON_INCLUDES}) -SET(CONTRACT_FAMILIES exchange-contract digital-asset-contract inference-contract) +SET(CONTRACT_FAMILIES exchange-contract digital-asset-contract inference-contract hfmodels-contract) # A local cmake file (Local.cmake) allows for local overrides of # variables. In particular, this is useful to set CONTRACT_FAMILIES diff --git a/hfmodels-contract/CMakeLists.txt b/hfmodels-contract/CMakeLists.txt new file mode 100644 index 0000000..1036627 --- /dev/null +++ b/hfmodels-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(hfmodels_common.cmake) +LIST(APPEND WASM_LIBRARIES ${HFMODELS_LIB}) +LIST(APPEND WASM_INCLUDES ${HFMODELS_INCLUDES}) + +ADD_LIBRARY(${HFMODELS_LIB} STATIC ${HFMODELS_SOURCES}) +TARGET_INCLUDE_DIRECTORIES(${HFMODELS_LIB} PUBLIC ${HFMODELS_INCLUDES}) + +SET_PROPERTY(TARGET ${HFMODELS_LIB} APPEND_STRING PROPERTY COMPILE_OPTIONS "${WASM_BUILD_OPTIONS}") +SET_PROPERTY(TARGET ${HFMODELS_LIB} APPEND_STRING PROPERTY LINK_OPTIONS "${WASM_LINK_OPTIONS}") +SET_TARGET_PROPERTIES(${HFMODELS_LIB} PROPERTIES EXCLUDE_FROM_ALL TRUE) + +BUILD_CONTRACT(hfmodels_token_object contracts/token_object.cpp) + +# ----------------------------------------------------------------- +INCLUDE(Python) +BUILD_WHEEL(hfmodels hfmodels_token_object) \ No newline at end of file diff --git a/hfmodels-contract/contracts/token_object.cpp b/hfmodels-contract/contracts/token_object.cpp new file mode 100644 index 0000000..2e7fd93 --- /dev/null +++ b/hfmodels-contract/contracts/token_object.cpp @@ -0,0 +1,78 @@ +/* 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 "hfmodels/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::hfmodels::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_model_info, ww::hfmodels::token_object::get_model_info), + CONTRACT_METHOD2(use_model, ww::hfmodels::token_object::use_model), + CONTRACT_METHOD2(get_capability, ww::hfmodels::token_object::get_capability), + + // 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/hfmodels-contract/hfmodels/contracts/token_object.cpp b/hfmodels-contract/hfmodels/contracts/token_object.cpp new file mode 100644 index 0000000..2d4cdb0 --- /dev/null +++ b/hfmodels-contract/hfmodels/contracts/token_object.cpp @@ -0,0 +1,318 @@ +/* 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 "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 "hfmodels/token_object.h" + + +static KeyValueStore hfmodel_TO_store("hfmodel_TO_store"); +static const std::string hfmodel_auth_token_KEY("hfmodel_auth_token"); +static const std::string hfmodel_endpoint_url_KEY("hfmodel_endpoint_url"); +static const std::string hfmodel_fixed_params_KEY("hfmodel_fixed_params_json_string"); +static const std::string hfmodel_user_inputs_schema_KEY("hfmodel_user_inputs_schema"); +static const std::string hfmodel_request_payload_type_KEY("hfmodel_request_payload_type"); +static const std::string hfmodel_usage_info_KEY("hfmodel_usage_info"); +static const std::string hfmodel_max_use_count_KEY("hfmodel_max_use_count"); +static const std::string hfmodel_current_use_count_KEY("hfmodel_current_use_count"); + +static const std::string model_use_capability_kv_store_encryption_key_KEY("model_use_capability_kv_store_encryption_key"); +static const std::string model_use_capability_kv_store_root_block_hash_KEY("model_use_capability_kv_store_root_block_hash"); +static const std::string model_use_capability_kv_store_input_key_KEY("model_use_capability_kv_store_input_key"); +static const std::string model_use_capability_user_inputs_KEY("model_use_capability_user_inputs"); + + +// ----------------------------------------------------------------- +// METHOD: initialize +// +// 1. Store model owner's authentication token that will be required to invoke Inference API. This is secret and shall never +// be exposed to anyone other than the model owner. +// 2. Store HF endpoint URL that will be used to invoke the Inference API. This is secret and shall never be exposed to anyone +// other than the model owner. +// 3. Store any fixed model parameters required to invoke Inference API. The model parameters is stored as a JSON string, not parsed by the TO. +// The guardian will parse the JSON string while invoking the inference API. There is no schema check. HF API server will return error +// if the parameters are not correct. +// 4. Store the payload type for the HF model request. Support types are json and binary. Use json while working with language models, +// and binary while working with image/audio models. If payload type is binary, the input data must be first send to the key_value +// store attached to the guardian. If payload type is json, use user_inputs_schema to specify the schema for the input data. +// The fixed model model parameters is used only when the payload type is json. Otherwise it is ignored by the guardian. +// See https://huggingface.co/docs/api-inference/en/detailed_parameters for examples. When payload type is json, the guardian +// will check user_inputs_schema to ensure that the input data is in the correct format before invoking the inference API. +// 5. Specify limit on the number of times the model can be used. Each use of the model will decrement the limit. +// 6. Store any other model metadata useful for the TO to understand how to use the model. Stored as string +// +// Note that the token object is intentionally kept generic and not hard-coded to any specific model. +// ------------------------------------------------------------------------------------------------------------- +bool ww::hfmodels::token_object::initialize(const Message& msg, const Environment& env, Response& rsp) +{ + ASSERT_SENDER_IS_CREATOR(env, rsp); + ASSERT_UNINITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(HFMODEL_TO_INITIALIZE_PARAM_SCHEMA), + "invalid request, missing required parameters for HF model token object initialize"); + + + //Get the params to be stored in hfmodel_TO_store + const std::string hfmodel_auth_token_value(msg.get_string("hf_auth_token")); + const std::string hfmodel_endpoint_url_value(msg.get_string("hf_endpoint_url")); + const std::string hfmodel_fixed_params_value(msg.get_string("fixed_model_params")); + const std::string hfmodel_user_inputs_schema_value(msg.get_string("user_inputs_schema")); + const std::string hfmodel_request_payload_type_value(msg.get_string("payload_type")); + const std::string hfmodel_usage_info_value(msg.get_string("hfmodel_usage_info")); + const uint32_t hfmodel_max_use_count_value = (uint32_t) msg.get_number("max_use_count"); + + //Store params from msg in hfmodel_TO_store + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(hfmodel_auth_token_KEY, hfmodel_auth_token_value), "failed to store hfmodel_auth_token"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(hfmodel_endpoint_url_KEY, hfmodel_endpoint_url_value), "failed to store hfmodel_endpoint_url"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(hfmodel_fixed_params_KEY, hfmodel_fixed_params_value), "failed to store hfmodel_fixed_params"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(hfmodel_user_inputs_schema_KEY, hfmodel_user_inputs_schema_value), "failed to store hfmodel_user_inputs_schema"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(hfmodel_request_payload_type_KEY, hfmodel_request_payload_type_value), "failed to store hfmodel_request_payload_type"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(hfmodel_usage_info_KEY, hfmodel_usage_info_value), "failed to store hfmodel_usage_info"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(hfmodel_max_use_count_KEY, hfmodel_max_use_count_value), "failed to store hfmodel_max_use_count"); + + //Set current use count to 0 + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(hfmodel_current_use_count_KEY, (uint32_t) 0), "failed to store hfmodel_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); +} + + + +// ----------------------------------------------------------------- +// METHOD: get_model_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::hfmodels::token_object::get_model_info( + const Message& msg, + const Environment& env, + Response& rsp) +{ + ASSERT_INITIALIZED(rsp); + ww::value::Structure v(MODEL_INFO_SCHEMA); + + // Get the payload type + std::string hfmodel_request_payload_type_string; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_request_payload_type_KEY, hfmodel_request_payload_type_string), "failed to retrieve hfmodel_request_payload_type"); + ASSERT_SUCCESS(rsp, v.set_string("payload_type", hfmodel_request_payload_type_string.c_str()), "failed to set return value for payload_type"); + + // Get the fixed model parameters + std::string hfmodel_fixed_params_string; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_fixed_params_KEY, hfmodel_fixed_params_string), "failed to retrieve hfmodel_fixed_params"); + ASSERT_SUCCESS(rsp, v.set_string("fixed_model_params", hfmodel_fixed_params_string.c_str()), "failed to set return value for hfmodel_fixed_params"); + + // Get the schema for user-specified inputs (used only when payload type is json) + std::string hfmodel_user_inputs_schema_string; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_user_inputs_schema_KEY, hfmodel_user_inputs_schema_string), "failed to retrieve hfmodel_user_inputs_schema"); + ASSERT_SUCCESS(rsp, v.set_string("user_inputs_schema", hfmodel_user_inputs_schema_string.c_str()), "failed to set return value for hfmodel_user_inputs_schema"); + + // Get the model metadata + std::string hfmodel_usage_info_string; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_usage_info_KEY, hfmodel_usage_info_string), "failed to retrieve hfmodel_usage_info"); + ASSERT_SUCCESS(rsp, v.set_string("hfmodel_usage_info", hfmodel_usage_info_string.c_str()), "failed to set return value for hfmodel_usage_info"); + + // Get the max use count + uint32_t hfmodel_max_use_count_value; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_max_use_count_KEY, hfmodel_max_use_count_value), "failed to retrieve hfmodel_max_use_count"); + ASSERT_SUCCESS(rsp, v.set_number("max_use_count", hfmodel_max_use_count_value), "failed to set return value for max_use_count"); + + return rsp.value(v, false); + +} + + +// ----------------------------------------------------------------- +// METHOD: use_model +// +// 1. Save the parameters required to generate a use_model 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 +// user_inputs +// 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::hfmodels::token_object::use_model( + const Message& msg, + const Environment& env, + Response& rsp) +{ + ASSERT_SENDER_IS_OWNER(env, rsp); + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(USE_MODEL_SCHEMA), "invalid request, missing required parameters"); + + 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")); + const std::string user_inputs(msg.get_string("user_inputs")); + + // check that current count < max count. Increment the current count. + // Note that we use < instead of <= since current count starts at 0. + uint32_t hfmodel_current_use_count_value; + uint32_t hfmodel_max_use_count_value; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_current_use_count_KEY, hfmodel_current_use_count_value), "failed to retrieve hfmodel_current_use_count"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_max_use_count_KEY, hfmodel_max_use_count_value), "failed to retrieve hfmodel_max_use_count"); + ASSERT_SUCCESS(rsp, hfmodel_current_use_count_value < hfmodel_max_use_count_value, "max use count is reached, cannot use model"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(hfmodel_current_use_count_KEY, hfmodel_current_use_count_value + 1), "failed to update hfmodel_current_use_count"); + + // store the parameters required to generate a use_model capability + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(model_use_capability_kv_store_encryption_key_KEY, kvstore_encryption_key), "failed to store model_use_capability_kv_store_enc_key"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(model_use_capability_kv_store_root_block_hash_KEY, kvstore_root_block_hash), "failed to store model_use_capability_kv_store_hash"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(model_use_capability_kv_store_input_key_KEY, kvstore_input_key), "failed to store model_use_capability_kv_store_input_key"); + ASSERT_SUCCESS(rsp, hfmodel_TO_store.set(model_use_capability_user_inputs_KEY, user_inputs), "failed to store model_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::hfmodels::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. + uint32_t hfmodel_current_use_count_value; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_current_use_count_KEY, hfmodel_current_use_count_value), "failed to retrieve hfmodel_current_use_count"); + ASSERT_SUCCESS(rsp, hfmodel_current_use_count_value > 0, "invalid request, capability can be obtained only after use_model 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 hfmodel_TO_store + std::string kvstore_encryption_key; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(model_use_capability_kv_store_encryption_key_KEY, kvstore_encryption_key), "failed to retrieve model_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 hfmodel_TO_store + std::string kvstore_root_block_hash; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(model_use_capability_kv_store_root_block_hash_KEY, kvstore_root_block_hash), "failed to retrieve model_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 hfmodel_TO_store + std::string kvstore_input_key; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(model_use_capability_kv_store_input_key_KEY, kvstore_input_key), "failed to retrieve model_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 payload_type from hfmodel_TO_store + std::string payload_type; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_request_payload_type_KEY, payload_type), "failed to retrieve hfmodel_request_payload_type"); + ASSERT_SUCCESS(rsp, params.set_string("payload_type", payload_type.c_str()), "failed to set return value for payload_type"); + + // Get user_inputs from hfmodel_TO_store + std::string user_inputs; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(model_use_capability_user_inputs_KEY, user_inputs), "failed to retrieve model_use_capability_user_inputs"); + ASSERT_SUCCESS(rsp, params.set_string("user_inputs", user_inputs.c_str()), "failed to set return value for user_inputs"); + + // Get hf_auth_token from hfmodel_TO_store + std::string hf_auth_token; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_auth_token_KEY, hf_auth_token), "failed to retrieve hf_auth_token"); + ASSERT_SUCCESS(rsp, params.set_string("hf_auth_token", hf_auth_token.c_str()), "failed to set return value for hf_auth_token"); + + // Get hf_endpoint_url from hfmodel_TO_store + std::string hf_endpoint_url; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_endpoint_url_KEY, hf_endpoint_url), "failed to retrieve hf_endpoint_url"); + ASSERT_SUCCESS(rsp, params.set_string("hf_endpoint_url", hf_endpoint_url.c_str()), "failed to set return value for hf_endpoint_url"); + + // Get fixed_model_params from hfmodel_TO_store + std::string fixed_model_params; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_fixed_params_KEY, fixed_model_params), "failed to retrieve fixed_model_params"); + ASSERT_SUCCESS(rsp, params.set_string("fixed_model_params", fixed_model_params.c_str()), "failed to set return value for fixed_model_params"); + + // Get user_inputs_schema from hfmodel_TO_store + std::string user_inputs_schema; + ASSERT_SUCCESS(rsp, hfmodel_TO_store.get(hfmodel_user_inputs_schema_KEY, user_inputs_schema), "failed to retrieve user_inputs_schema"); + ASSERT_SUCCESS(rsp, params.set_string("user_inputs_schema", user_inputs_schema.c_str()), "failed to set return value for user_inputs_schema"); + + // Calculate capability + ww::value::Object result; + ASSERT_SUCCESS(rsp, ww::exchange::token_object::create_operation_package("use_hfmodel", params, result), + "unexpected error: failed to generate capability"); + + // this assumes that generating the capability does not change state, depending on + return rsp.value(result, false); + +} \ No newline at end of file diff --git a/hfmodels-contract/hfmodels/token_object.h b/hfmodels-contract/hfmodels/token_object.h new file mode 100644 index 0000000..106527f --- /dev/null +++ b/hfmodels-contract/hfmodels/token_object.h @@ -0,0 +1,91 @@ +/* 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 "Util.h" +#include "Secret.h" +#include "exchange/token_object.h" +#include "exchange/issuer_authority_base.h" + + +#define HFMODEL_TO_INITIALIZE_PARAM_SCHEMA \ + "{" \ + SCHEMA_KW(hf_auth_token, "") "," \ + SCHEMA_KW(hf_endpoint_url, "") "," \ + SCHEMA_KW(fixed_model_params, "") "," \ + SCHEMA_KW(user_inputs_schema, "") "," \ + SCHEMA_KW(payload_type, "") "," \ + SCHEMA_KW(hfmodel_usage_info, "") "," \ + SCHEMA_KW(max_use_count, 0) "," \ + SCHEMA_KW(ledger_verifying_key, "") "," \ + SCHEMA_KWS(initialization_package, CONTRACT_SECRET_SCHEMA) "," \ + SCHEMA_KWS(asset_authority_chain, ISSUER_AUTHORITY_CHAIN_SCHEMA)\ + "}" + +#define MODEL_INFO_SCHEMA \ + "{" \ + SCHEMA_KW(fixed_model_params, "") "," \ + SCHEMA_KW(user_inputs_schema, "") "," \ + SCHEMA_KW(payload_type, "") "," \ + SCHEMA_KW(hfmodel_usage_info, "") "," \ + SCHEMA_KW(max_use_count, "") \ + "}" + +#define USE_MODEL_SCHEMA \ + "{" \ + SCHEMA_KW(kvstore_encryption_key, "") "," \ + SCHEMA_KW(kvstore_root_block_hash, "") "," \ + SCHEMA_KW(kvstore_input_key, "") "," \ + SCHEMA_KW(user_inputs, "") \ + "}" + +#define GET_CAPABILITY_SCHEMA \ + "{" \ + 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(hf_auth_token, "") "," \ + SCHEMA_KW(hf_endpoint_url, "") "," \ + SCHEMA_KW(payload_type, "") "," \ + SCHEMA_KW(fixed_model_params, "") "," \ + SCHEMA_KW(user_inputs_schema, "") "," \ + SCHEMA_KW(user_inputs, "") \ + "}" + + + +namespace ww +{ +namespace hfmodels +{ +namespace token_object +{ + // methods + bool initialize(const Message& msg, const Environment& env, Response& rsp); + bool get_model_info(const Message& msg, const Environment& env, Response& rsp); + bool use_model(const Message& msg, const Environment& env, Response& rsp); + bool get_capability(const Message& msg, const Environment& env, Response& rsp); +}; // token_object +}; // hfmodels +}; // ww diff --git a/hfmodels-contract/hfmodels_common.cmake b/hfmodels-contract/hfmodels_common.cmake new file mode 100644 index 0000000..e89c8ca --- /dev/null +++ b/hfmodels-contract/hfmodels_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 (HFMODELS_INCLUDES ${WASM_INCLUDES}) +LIST(APPEND HFMODELS_INCLUDES ${EXCHANGE_INCLUDES}) +LIST(APPEND HFMODELS_INCLUDES ${CMAKE_CURRENT_LIST_DIR}) + +# --------------------------------------------- +# Set up the default source list +# --------------------------------------------- +FILE(GLOB HFMODELS_COMMON_SOURCE ${CMAKE_CURRENT_LIST_DIR}/hfmodels/common/*.cpp) +FILE(GLOB HFMODELS_CONTRACT_SOURCE ${CMAKE_CURRENT_LIST_DIR}/hfmodels/contracts/*.cpp) + +SET (HFMODELS_SOURCES) +LIST(APPEND HFMODELS_SOURCES ${HFMODELS_COMMON_SOURCE}) +LIST(APPEND HFMODELS_SOURCES ${HFMODELS_CONTRACT_SOURCE}) + +# --------------------------------------------- +# Build the wawaka contract common library +# --------------------------------------------- +SET(HFMODELS_LIB ww_hfmodels) diff --git a/inference-contract/inference_common.cmake b/inference-contract/inference_common.cmake index 8246513..81e6bf1 100644 --- a/inference-contract/inference_common.cmake +++ b/inference-contract/inference_common.cmake @@ -19,7 +19,7 @@ ENDIF() # --------------------------------------------- # Set up the include list # --------------------------------------------- -SET (INFERENCE__INCLUDES ${WASM_INCLUDES}) +SET (INFERENCE_INCLUDES ${WASM_INCLUDES}) LIST(APPEND INFERENCE_INCLUDES ${EXCHANGE_INCLUDES}) LIST(APPEND INFERENCE_INCLUDES ${CMAKE_CURRENT_LIST_DIR}) From 8a6df2ec16b801cf35f19c2cbab2c011d53c6a00 Mon Sep 17 00:00:00 2001 From: Prakash Narayana Moorthy Date: Mon, 8 Jul 2024 04:16:18 +0000 Subject: [PATCH 3/5] Guardian web server (frontend) for tokenization of Hugging Face models. The Guardian frontend is largely similar to the guardian used as part of the OpenVINO inference contract use-case. The operations folder implements the capability_handler_map unique to the Hugging Face use case. The use_hfmodel module implemented within the operations package enables the guardian server to process inferencing capabilities that invoke REST API calls to the Hugging Face hosted models. Parameters required for the API call are passed as part of the capability package. The module implements support for JSON and binary payloads. The module is model agnostic, and does not implement any model specific pre or post processing steps. The large overlap among the remaining modules of the HF and OpenVINO guardians calls for a future PR that refactors the modules to permit reuse. Currently, simply expecting the HF use case to reuse the OpenVINO guardian frontend python packages is challenging, since OpenVINO guardian demands intallation of dependencies such an tensoflow, opencv, numpy etc, none of which are required for the HF usecase. In a secure deployment, the guardian is ideally deployed with a TEEs, and hence it's best to have a SW footprint that is minimal. Signed-off-by: Prakash Narayana Moorthy --- .../pdo/hfmodels/common/__init__.py | 22 ++ .../pdo/hfmodels/common/capability_keys.py | 118 +++++++ .../hfmodels/common/capability_keystore.py | 57 ++++ .../pdo/hfmodels/common/endpoint_registry.py | 44 +++ .../pdo/hfmodels/common/guardian_service.py | 136 ++++++++ .../pdo/hfmodels/common/secrets.py | 87 +++++ .../pdo/hfmodels/common/utility.py | 23 ++ .../pdo/hfmodels/operations/__init__.py | 22 ++ .../pdo/hfmodels/operations/use_hfmodel.py | 133 ++++++++ .../pdo/hfmodels/resources/__init__.py | 15 + .../pdo/hfmodels/resources/resources.py | 18 + .../pdo/hfmodels/scripts/__init__.py | 0 .../pdo/hfmodels/scripts/guardianCLI.py | 313 ++++++++++++++++++ .../pdo/hfmodels/scripts/scripts.py | 31 ++ .../pdo/hfmodels/wsgi/__init__.py | 36 ++ .../pdo/hfmodels/wsgi/add_endpoint.py | 114 +++++++ hfmodels-contract/pdo/hfmodels/wsgi/info.py | 56 ++++ .../pdo/hfmodels/wsgi/process_capability.py | 118 +++++++ .../hfmodels/wsgi/provision_token_issuer.py | 76 +++++ .../hfmodels/wsgi/provision_token_object.py | 96 ++++++ 20 files changed, 1515 insertions(+) create mode 100644 hfmodels-contract/pdo/hfmodels/common/__init__.py create mode 100644 hfmodels-contract/pdo/hfmodels/common/capability_keys.py create mode 100644 hfmodels-contract/pdo/hfmodels/common/capability_keystore.py create mode 100644 hfmodels-contract/pdo/hfmodels/common/endpoint_registry.py create mode 100644 hfmodels-contract/pdo/hfmodels/common/guardian_service.py create mode 100644 hfmodels-contract/pdo/hfmodels/common/secrets.py create mode 100644 hfmodels-contract/pdo/hfmodels/common/utility.py create mode 100644 hfmodels-contract/pdo/hfmodels/operations/__init__.py create mode 100644 hfmodels-contract/pdo/hfmodels/operations/use_hfmodel.py create mode 100644 hfmodels-contract/pdo/hfmodels/resources/__init__.py create mode 100644 hfmodels-contract/pdo/hfmodels/resources/resources.py create mode 100644 hfmodels-contract/pdo/hfmodels/scripts/__init__.py create mode 100644 hfmodels-contract/pdo/hfmodels/scripts/guardianCLI.py create mode 100644 hfmodels-contract/pdo/hfmodels/scripts/scripts.py create mode 100644 hfmodels-contract/pdo/hfmodels/wsgi/__init__.py create mode 100644 hfmodels-contract/pdo/hfmodels/wsgi/add_endpoint.py create mode 100644 hfmodels-contract/pdo/hfmodels/wsgi/info.py create mode 100644 hfmodels-contract/pdo/hfmodels/wsgi/process_capability.py create mode 100644 hfmodels-contract/pdo/hfmodels/wsgi/provision_token_issuer.py create mode 100644 hfmodels-contract/pdo/hfmodels/wsgi/provision_token_object.py diff --git a/hfmodels-contract/pdo/hfmodels/common/__init__.py b/hfmodels-contract/pdo/hfmodels/common/__init__.py new file mode 100644 index 0000000..c595226 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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/hfmodels-contract/pdo/hfmodels/common/capability_keys.py b/hfmodels-contract/pdo/hfmodels/common/capability_keys.py new file mode 100644 index 0000000..dbc6cee --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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/hfmodels-contract/pdo/hfmodels/common/capability_keystore.py b/hfmodels-contract/pdo/hfmodels/common/capability_keystore.py new file mode 100644 index 0000000..a8d33cd --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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.hfmodels.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/hfmodels-contract/pdo/hfmodels/common/endpoint_registry.py b/hfmodels-contract/pdo/hfmodels/common/endpoint_registry.py new file mode 100644 index 0000000..297c0c3 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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/hfmodels-contract/pdo/hfmodels/common/guardian_service.py b/hfmodels-contract/pdo/hfmodels/common/guardian_service.py new file mode 100644 index 0000000..d3361b0 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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/hfmodels-contract/pdo/hfmodels/common/secrets.py b/hfmodels-contract/pdo/hfmodels/common/secrets.py new file mode 100644 index 0000000..680b79b --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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.hfmodels.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.hfmodels.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.hfmodels.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/hfmodels-contract/pdo/hfmodels/common/utility.py b/hfmodels-contract/pdo/hfmodels/common/utility.py new file mode 100644 index 0000000..f0d53cf --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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/hfmodels-contract/pdo/hfmodels/operations/__init__.py b/hfmodels-contract/pdo/hfmodels/operations/__init__.py new file mode 100644 index 0000000..4030fa9 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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_hfmodel'] + +from pdo.hfmodels.operations.use_hfmodel import HFModelOperation + +capability_handler_map = { + 'use_hfmodel' : HFModelOperation, +} diff --git a/hfmodels-contract/pdo/hfmodels/operations/use_hfmodel.py b/hfmodels-contract/pdo/hfmodels/operations/use_hfmodel.py new file mode 100644 index 0000000..e3c58d0 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/operations/use_hfmodel.py @@ -0,0 +1,133 @@ +# 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. +""" + +from pdo.hfmodels.common.utility import ValidateJSON +from pdo.common.key_value import KeyValueStore + +import logging +import requests +import urllib.request +import json +logger = logging.getLogger(__name__) + + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class HFModelOperation(object) : + # ----------------------------------------------------------------- + __schema__ = { + "type" : "object", + "properties" : { + "kvstore_encryption_key" : { "type" : "string" }, + "kvstore_root_block_hash" : { "type" : "string" }, + "kvstore_input_key" : { "type" : "string" }, + "hf_auth_token" : { "type" : "string" }, + "hf_endpoint_url" : { "type" : "string" }, + "payload_type" : { "type" : "string" }, + "user_inputs" : { "type" : "string" }, + "user_inputs_schema" : { "type" : "string" }, + "fixed_model_params" : { "type" : "string" }, + } + } + + + # ----------------------------------------------------------------- + def __init__(self, config) : + pass + + @staticmethod + def query_hf_model(hf_endpoint_url, hf_auth_token, 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 {hf_auth_token}"} + try: + if payload_type == 'json': + response = requests.post(hf_endpoint_url, headers=headers, json=payload, proxies=urllib.request.getproxies()) + elif payload_type == 'binary': + response = requests.post(hf_endpoint_url, 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'] + kvstore_input_key = params['kvstore_input_key'] + hf_auth_token = params['hf_auth_token'] + hf_endpoint_url = params['hf_endpoint_url'] + payload_type = params['payload_type'] + user_inputs = json.loads(params['user_inputs']) + user_inputs_schema = json.loads(params['user_inputs_schema']) + fixed_model_params = json.loads(params['fixed_model_params']) + + + # If payload type is binary, get the input data from the key-value store. + if payload_type == 'binary': + kv = KeyValueStore(kvstore_encryption_key, kvstore_root_block_hash) + with kv: + input_data = kv.get(kvstore_input_key, output_encoding='raw') + payload = bytes(input_data) + elif payload_type == 'json': + # check schema of user inputs, and generate payload by merging user inputs with fixed model parameters + if not ValidateJSON(user_inputs, user_inputs_schema) : + logger.error("Invalid user inputs") + return None + payload = {**fixed_model_params, **user_inputs} + else: + logger.error("Invalid payload type. Supported types are 'json' and 'binary'") + return None + + # query the Hugging Face model and return the response + return self.query_hf_model(hf_endpoint_url, hf_auth_token, payload, payload_type) + + # to do: (Support for post processing) + # Add support for optionally encrypting response before sending back to the client + # encryption key specified by token object + # token object can implement policies for endorsing encryption keys + # for example: results encrypted for use within another PDO contract or even the token object itself + # Such a feature can be used for privacy-preserving post processing of the model outputs + # before sharing final results with the client + # If there are generic post processing steps that can be applied to a large class of models, + # the code can be added here itself. + \ No newline at end of file diff --git a/hfmodels-contract/pdo/hfmodels/resources/__init__.py b/hfmodels-contract/pdo/hfmodels/resources/__init__.py new file mode 100644 index 0000000..d0b2a5a --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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/hfmodels-contract/pdo/hfmodels/resources/resources.py b/hfmodels-contract/pdo/hfmodels/resources/resources.py new file mode 100644 index 0000000..ad35db8 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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_hfmodels_contract_resources() : + pinstaller.install_plugin_resources('pdo.hfmodels.resources', 'hfmodels') diff --git a/hfmodels-contract/pdo/hfmodels/scripts/__init__.py b/hfmodels-contract/pdo/hfmodels/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hfmodels-contract/pdo/hfmodels/scripts/guardianCLI.py b/hfmodels-contract/pdo/hfmodels/scripts/guardianCLI.py new file mode 100644 index 0000000..1c28140 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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.hfmodels.wsgi import wsgi_operation_map +from pdo.hfmodels.common.capability_keystore import CapabilityKeyStore +from pdo.hfmodels.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.hfmodels.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/hfmodels-contract/pdo/hfmodels/scripts/scripts.py b/hfmodels-contract/pdo/hfmodels/scripts/scripts.py new file mode 100644 index 0000000..a76dd5d --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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 hfmodels_token() : + run_shell_command('do_hfmodels_token', 'pdo.hfmodels.plugins.hfmodels_token_object') + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def hfmodels_guardian() : + run_shell_command('do_hfmodels_guardian', 'pdo.hfmodels.plugins.hfmodels_guardian') + diff --git a/hfmodels-contract/pdo/hfmodels/wsgi/__init__.py b/hfmodels-contract/pdo/hfmodels/wsgi/__init__.py new file mode 100644 index 0000000..cf2d5d8 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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.hfmodels.wsgi.add_endpoint import AddEndpointApp +from pdo.hfmodels.wsgi.info import InfoApp +from pdo.hfmodels.wsgi.process_capability import ProcessCapabilityApp +from pdo.hfmodels.wsgi.provision_token_issuer import ProvisionTokenIssuerApp +from pdo.hfmodels.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/hfmodels-contract/pdo/hfmodels/wsgi/add_endpoint.py b/hfmodels-contract/pdo/hfmodels/wsgi/add_endpoint.py new file mode 100644 index 0000000..010d771 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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.hfmodels.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/hfmodels-contract/pdo/hfmodels/wsgi/info.py b/hfmodels-contract/pdo/hfmodels/wsgi/info.py new file mode 100644 index 0000000..96c3eee --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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/hfmodels-contract/pdo/hfmodels/wsgi/process_capability.py b/hfmodels-contract/pdo/hfmodels/wsgi/process_capability.py new file mode 100644 index 0000000..5e006c6 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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.hfmodels.common.utility import ValidateJSON +from pdo.hfmodels.common.secrets import recv_secret +from pdo.hfmodels.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/hfmodels-contract/pdo/hfmodels/wsgi/provision_token_issuer.py b/hfmodels-contract/pdo/hfmodels/wsgi/provision_token_issuer.py new file mode 100644 index 0000000..86602ba --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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.hfmodels.common.utility import ValidateJSON +from pdo.hfmodels.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/hfmodels-contract/pdo/hfmodels/wsgi/provision_token_object.py b/hfmodels-contract/pdo/hfmodels/wsgi/provision_token_object.py new file mode 100644 index 0000000..b005dc9 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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.hfmodels.common.utility import ValidateJSON +from pdo.hfmodels.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] From d93674e826bbf2bbf3a9051b85027041f28b73fe Mon Sep 17 00:00:00 2001 From: Prakash Narayana Moorthy Date: Mon, 8 Jul 2024 04:31:52 +0000 Subject: [PATCH 4/5] 1. Client plugins for the Hugging Face PDO contract use case. The guardian plugin is largely same as the guardian plugin for the openvino inference use case. A future PR that permits refactoring, and reuse of modules among the HF and OpenVINO use cases needs to be explored. 2. installation related files such as setup.py, MANIFEST, etc. 3. test script that needs to be manually invoked to test the HF use case. To run the test script, the user must create an HF account, and obtain an HF authentication token, and set HF_AUTH_TOKEN environment variable to the token value. Due to this external depdency, test is currently not integrated as part of the 'make test' automatic test suite. The test is done using the gpt2 opensource model available on Hugging Face. Signed-off-by: Prakash Narayana Moorthy --- hfmodels-contract/MANIFEST | 32 ++ hfmodels-contract/MANIFEST.in | 4 + hfmodels-contract/context/tokens.toml | 54 +++ hfmodels-contract/etc/guardian_service.toml | 69 +++ hfmodels-contract/etc/hfmodels.toml | 19 + hfmodels-contract/pdo/hfmodels/__init__.py | 15 + .../pdo/hfmodels/plugins/__init__.py | 15 + .../pdo/hfmodels/plugins/hfmodels_guardian.py | 286 +++++++++++ .../hfmodels/plugins/hfmodels_token_object.py | 451 ++++++++++++++++++ hfmodels-contract/scripts/gs_start.sh | 90 ++++ hfmodels-contract/scripts/gs_status.sh | 36 ++ hfmodels-contract/scripts/gs_stop.sh | 48 ++ hfmodels-contract/scripts/ss_start.sh | 77 +++ hfmodels-contract/scripts/ss_status.sh | 36 ++ hfmodels-contract/scripts/ss_stop.sh | 48 ++ hfmodels-contract/setup.py | 102 ++++ hfmodels-contract/test/.gitignore | 1 + hfmodels-contract/test/script_test.sh | 225 +++++++++ private-data-objects | 2 +- 19 files changed, 1609 insertions(+), 1 deletion(-) create mode 100644 hfmodels-contract/MANIFEST create mode 100644 hfmodels-contract/MANIFEST.in create mode 100644 hfmodels-contract/context/tokens.toml create mode 100644 hfmodels-contract/etc/guardian_service.toml create mode 100644 hfmodels-contract/etc/hfmodels.toml create mode 100644 hfmodels-contract/pdo/hfmodels/__init__.py create mode 100644 hfmodels-contract/pdo/hfmodels/plugins/__init__.py create mode 100644 hfmodels-contract/pdo/hfmodels/plugins/hfmodels_guardian.py create mode 100644 hfmodels-contract/pdo/hfmodels/plugins/hfmodels_token_object.py create mode 100755 hfmodels-contract/scripts/gs_start.sh create mode 100755 hfmodels-contract/scripts/gs_status.sh create mode 100755 hfmodels-contract/scripts/gs_stop.sh create mode 100755 hfmodels-contract/scripts/ss_start.sh create mode 100755 hfmodels-contract/scripts/ss_status.sh create mode 100755 hfmodels-contract/scripts/ss_stop.sh create mode 100644 hfmodels-contract/setup.py create mode 100644 hfmodels-contract/test/.gitignore create mode 100755 hfmodels-contract/test/script_test.sh diff --git a/hfmodels-contract/MANIFEST b/hfmodels-contract/MANIFEST new file mode 100644 index 0000000..9e44a49 --- /dev/null +++ b/hfmodels-contract/MANIFEST @@ -0,0 +1,32 @@ +MANIFEST.in +./setup.py +./pdo/hfmodels/wsgi/provision_token_issuer.py +./pdo/hfmodels/wsgi/__init__.py +./pdo/hfmodels/wsgi/add_endpoint.py +./pdo/hfmodels/wsgi/provision_token_object.py +./pdo/hfmodels/wsgi/info.py +./pdo/hfmodels/wsgi/process_capability.py +./pdo/hfmodels/operations/use_hfmodel.py +./pdo/hfmodels/operations/__init__.py +./pdo/hfmodels/plugins/hfmodels_token_object.py +./pdo/hfmodels/plugins/__init__.py +./pdo/hfmodels/plugins/hfmodels_guardian.py +./pdo/hfmodels/__init__.py +./pdo/hfmodels/resources/resources.py +./pdo/hfmodels/resources/__init__.py +./pdo/hfmodels/common/guardian_service.py +./pdo/hfmodels/common/capability_keystore.py +./pdo/hfmodels/common/secrets.py +./pdo/hfmodels/common/__init__.py +./pdo/hfmodels/common/endpoint_registry.py +./pdo/hfmodels/common/utility.py +./pdo/hfmodels/common/capability_keys.py +./pdo/hfmodels/scripts/__init__.py +./pdo/hfmodels/scripts/guardianCLI.py +./pdo/hfmodels/scripts/scripts.py +./scripts/gs_stop.sh +./scripts/gs_start.sh +./scripts/gs_status.sh +./context/tokens.toml +./etc/hfmodels.toml +./etc/guardian_service.toml \ No newline at end of file diff --git a/hfmodels-contract/MANIFEST.in b/hfmodels-contract/MANIFEST.in new file mode 100644 index 0000000..918cee3 --- /dev/null +++ b/hfmodels-contract/MANIFEST.in @@ -0,0 +1,4 @@ +recursive-include ../build/hfmodels-contract *.b64 +recursive-include etc *.toml +recursive-include context *.toml +recursive-include scripts *.sh diff --git a/hfmodels-contract/context/tokens.toml b/hfmodels-contract/context/tokens.toml new file mode 100644 index 0000000..42032a7 --- /dev/null +++ b/hfmodels-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.hfmodels.plugins.hfmodels_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.hfmodels.plugins.hfmodels_token_object" +identity = "${..token_issuer.identity}" +source = "${ContractFamily.hfmodels.token_object.source}" +token_issuer_context = "@{..token_issuer}" +data_guardian_context = "@{..guardian}" diff --git a/hfmodels-contract/etc/guardian_service.toml b/hfmodels-contract/etc/guardian_service.toml new file mode 100644 index 0000000..c3d115a --- /dev/null +++ b/hfmodels-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/hfmodels-contract/etc/hfmodels.toml b/hfmodels-contract/etc/hfmodels.toml new file mode 100644 index 0000000..99995e1 --- /dev/null +++ b/hfmodels-contract/etc/hfmodels.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. + +# ----------------------------------------------------------------- +# HFMODELS family contract source +# ----------------------------------------------------------------- +[ContractFamily.hfmodels] +token_object = { source = "${home}/contracts/hfmodels/_hfmodels_token_object.b64" } diff --git a/hfmodels-contract/pdo/hfmodels/__init__.py b/hfmodels-contract/pdo/hfmodels/__init__.py new file mode 100644 index 0000000..c0dfac2 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/__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/hfmodels-contract/pdo/hfmodels/plugins/__init__.py b/hfmodels-contract/pdo/hfmodels/plugins/__init__.py new file mode 100644 index 0000000..fdbdb53 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/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__ = [ 'hfmodels_guardian', 'hfmodels_token_object'] diff --git a/hfmodels-contract/pdo/hfmodels/plugins/hfmodels_guardian.py b/hfmodels-contract/pdo/hfmodels/plugins/hfmodels_guardian.py new file mode 100644 index 0000000..a26e33a --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/plugins/hfmodels_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. + +""" +hfmodels 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.hfmodels.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_hfmodels_guardian', + 'do_hfmodels_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 hfmodels 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 hfmodels 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 hfmodels 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 hfmodels 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 hfmodels 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_hfmodels_guardian_service = pcontract.create_shell_command('hfmodels_guardian_service', __operations__) + +__commands__ = [ + cmd_provision_token_issuer, + cmd_provision_token_object, +] + +do_hfmodels_guardian = pcommand.create_shell_command('hfmodels_guardian', __commands__) + +## ----------------------------------------------------------------- +## Enable binding of the shell independent version to a pdo-shell command +## ----------------------------------------------------------------- +def load_commands(cmdclass) : + pshell.bind_shell_command(cmdclass, 'hfmodels_guardian', do_hfmodels_guardian) + pshell.bind_shell_command(cmdclass, 'hfmodels_guardian_service', do_hfmodels_guardian_service) \ No newline at end of file diff --git a/hfmodels-contract/pdo/hfmodels/plugins/hfmodels_token_object.py b/hfmodels-contract/pdo/hfmodels/plugins/hfmodels_token_object.py new file mode 100644 index 0000000..d4832f6 --- /dev/null +++ b/hfmodels-contract/pdo/hfmodels/plugins/hfmodels_token_object.py @@ -0,0 +1,451 @@ +# 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 + +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.hfmodels.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_modelinfo', + 'op_use_hfmodel', + 'op_get_capability', + 'cmd_mint_hf_tokens', + 'cmd_mint_tokens', + 'cmd_transfer_assets', + 'cmd_use_hfmodel', + 'cmd_get_modelinfo', + 'do_hfmodels_token', + 'do_hfmodels_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('--hf_auth_token', help='Name of the contract class', type=str) + subparser.add_argument('--hf_endpoint_url', help='Name of the enclave service group to use', type=str) + subparser.add_argument('--fixed_model_params', help='File where contract data is stored', 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('--hfmodel_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['hf_auth_token'] = kwargs.get('hf_auth_token') + params['hf_endpoint_url'] = kwargs.get('hf_endpoint_url') + params['fixed_model_params'] = kwargs.get('fixed_model_params') + params['user_inputs_schema'] = kwargs.get('user_inputs_schema') + params['payload_type'] = kwargs.get('payload_type', 'json') + params['hfmodel_usage_info'] = kwargs.get('hfmodel_usage_info') + params['max_use_count'] = kwargs.get('max_use_count', 1) + + 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_modelinfo(pcontract.contract_op_base) : + """op_get_modelinfo implements the method to get the info about the Hugging Face model stored in the token object + """ + + name = "op_get_modelinfo" + help = "get info about the Hugging Face model stored in the token object" + + @classmethod + def invoke(cls, state, session_params, **kwargs) : + session_params['commit'] = False + + params = {} + message = invocation_request('get_model_info', **params) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_use_hfmodel(pcontract.contract_op_base) : + """op_use_hfmodel implements step 1 for the usage of a Hugging Face model 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_hfmodel" + help = "implement step 1 of inference using Hugging Face model" + + @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( + '--user_inputs', + help='User inputs for the model, used when the payload is JSON', + type=str) + + @classmethod + def invoke(cls, state, session_params, kvstore_encryption_key, kvstore_input_key, kvstore_root_block_hash, user_inputs, **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['user_inputs'] = user_inputs + + message = invocation_request('use_model', **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 usage of a Hugging Face model 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 inference using Hugging Face model" + + @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) + + @classmethod + def invoke(cls, state, session_params, ledger_attestation, **kwargs) : + session_params['commit'] = False + + params = {} + params['ledger_signature'] = ledger_attestation + + message = invocation_request('get_capability', **params) + capability = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, capability) + + return capability + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_use_hfmodel(pcommand.contract_command_base) : + """cmd_use_hfmodel implements implements the usage of a Hugging Face model via the guardian service + """ + name = "use_hfmodel" + help = "inference using Hugging Face model" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '--data_file', + help='Filename of the data_file to use as inference input. this is used only if payload is binary', + type=str) + + subparser.add_argument( + '--search-path', + help='Directories to search for the data file', + nargs='+', type=str, default=['.', './data']) + + subparser.add_argument( + '--user_inputs', + help='User inputs for the model, used when the payload is JSON', + type=str) + + @classmethod + def invoke(cls, state, context, data_file=None, search_path=None, user_inputs=None, **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_modelinfo, + state, context, session, + **kwargs) + model_info = json.loads(model_info_json) + payload_type = model_info['payload_type'] + + if payload_type == 'json' and user_inputs is None: + raise ValueError("user inputs is required for json payloads") + + if payload_type == 'binary' and data_file is None: + raise ValueError("data file is required for binary payloads") + + # store the data file in the KV store if binary data is provided + if payload_type == 'binary': + data_file = putils.find_file_in_path(data_file, search_path) + with open(data_file, 'rb') as bf : + data_bytes = bf.read() + + data_key = crypto.string_to_byte_array("__data__") + data_bytes = crypto.string_to_byte_array(data_bytes) + + kv = KeyValueStore() + with kv : + _ = kv.set(data_key, data_bytes, input_encoding='raw', output_encoding='raw') + + kvstore_encryption_key = kv.encryption_key + kvstore_input_key = "__data__" + kvstore_root_block_hash = kv.hash_identity + user_inputs = "not_used" # this is not used in this case when payload is binary + else: + kvstore_encryption_key = "not_used" + kvstore_input_key = "not_used" + kvstore_root_block_hash = "not_used" + + # invoke op_use_model to store the parameters required to generate the capability + session = pbuilder.SessionParameters(save_file=save_file) + try: + _ = pcontract.invoke_contract_op( + op_use_hfmodel, + state, context, session, + kvstore_encryption_key, + kvstore_input_key, + kvstore_root_block_hash, + user_inputs, + **kwargs) + except Exception as e: + cls.display_error("op_use_hfmodel 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 + + 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) + if payload_type == 'binary': + kv.sync_to_block_store(service_client) + + # send capability to guardian service + capability = json.loads(capability) + result = service_client.process_capability(**capability) + cls.display(result) + return result + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_mint_hf_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_hf_tokens" + help = "mint tokens for a token issuer" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument('--hf_auth_token', help='Name of the contract class', type=str) + subparser.add_argument('--hf_endpoint_url', help='Name of the enclave service group to use', type=str) + subparser.add_argument('--fixed_model_params', help='File where contract data is stored', 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('--hfmodel_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, context, **kwargs) : + return pcommand.invoke_contract_cmd(cmd_mint_tokens, state, context, **kwargs) + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_get_modelinfo(pcommand.contract_command_base) : + """cmd_get_modelinfo 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_modelinfo" + help = "get model info of the Hugging Face model 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_modelinfo, + state, context, session, + **kwargs) + + 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_hfmodel, + op_get_modelinfo, + op_get_capability +] + +do_hfmodels_token_contract = pcontract.create_shell_command('hfmodels_token_contract', __operations__) + +__commands__ = [ + cmd_mint_hf_tokens, + cmd_transfer_assets, + cmd_use_hfmodel, + cmd_get_modelinfo, +] + +do_hfmodels_token = pcommand.create_shell_command('hfmodels_token', __commands__) + +## ----------------------------------------------------------------- +## Enable binding of the shell independent version to a pdo-shell command +## ----------------------------------------------------------------- +def load_commands(cmdclass) : + pshell.bind_shell_command(cmdclass, 'hfmodels_token', do_hfmodels_token) + pshell.bind_shell_command(cmdclass, 'hfmodels_token_contract', do_hfmodels_token_contract) + \ No newline at end of file diff --git a/hfmodels-contract/scripts/gs_start.sh b/hfmodels-contract/scripts/gs_start.sh new file mode 100755 index 0000000..df83264 --- /dev/null +++ b/hfmodels-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=hfmodel_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/hfmodels-contract/scripts/gs_status.sh b/hfmodels-contract/scripts/gs_status.sh new file mode 100755 index 0000000..b3adcac --- /dev/null +++ b/hfmodels-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=hfmodel_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/hfmodels-contract/scripts/gs_stop.sh b/hfmodels-contract/scripts/gs_stop.sh new file mode 100755 index 0000000..b581822 --- /dev/null +++ b/hfmodels-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=hfmodel_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/hfmodels-contract/scripts/ss_start.sh b/hfmodels-contract/scripts/ss_start.sh new file mode 100755 index 0000000..415e0bb --- /dev/null +++ b/hfmodels-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/hfmodels-contract/scripts/ss_status.sh b/hfmodels-contract/scripts/ss_status.sh new file mode 100755 index 0000000..0f6f13e --- /dev/null +++ b/hfmodels-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/hfmodels-contract/scripts/ss_stop.sh b/hfmodels-contract/scripts/ss_stop.sh new file mode 100755 index 0000000..5732aa1 --- /dev/null +++ b/hfmodels-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/hfmodels-contract/setup.py b/hfmodels-contract/setup.py new file mode 100644 index 0000000..618b847 --- /dev/null +++ b/hfmodels-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_hfmodels', + version=pdo_contracts_version, + description='Implementation of guardian service for Hugging Face models', + author='Prakash Narayana Moorthy, Intel Labs', + author_email='prakash.narayana.moorthy@intel.com', + url='http://www.intel.com', + package_dir = { + 'pdo' : 'pdo', + 'pdo.hfmodels.resources.etc' : 'etc', + 'pdo.hfmodels.resources.context' : 'context', + 'pdo.hfmodels.resources.contracts' : '../build/hfmodels-contract', + 'pdo.hfmodels.resources.scripts' : 'scripts', + }, + packages = [ + 'pdo', + 'pdo.hfmodels', + 'pdo.hfmodels.plugins', + 'pdo.hfmodels.scripts', + 'pdo.hfmodels.common', + 'pdo.hfmodels.operations', + 'pdo.hfmodels.wsgi', + 'pdo.hfmodels.resources', + 'pdo.hfmodels.resources.etc', + 'pdo.hfmodels.resources.context', + 'pdo.hfmodels.resources.contracts', + 'pdo.hfmodels.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' : [ + 'hfmodel_token=pdo.hfmodels.scripts.scripts:hfmodels_token', + 'hfmodel_guardian_service=pdo.hfmodels.scripts.guardianCLI:Main', + ] + } +) diff --git a/hfmodels-contract/test/.gitignore b/hfmodels-contract/test/.gitignore new file mode 100644 index 0000000..eb0abc5 --- /dev/null +++ b/hfmodels-contract/test/.gitignore @@ -0,0 +1 @@ +test_context.toml diff --git a/hfmodels-contract/test/script_test.sh b/hfmodels-contract/test/script_test.sh new file mode 100755 index 0000000..fe79159 --- /dev/null +++ b/hfmodels-contract/test/script_test.sh @@ -0,0 +1,225 @@ +#!/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}" +: "${HF_AUTH_TOKEN?Missing environment variable HF_AUTH_TOKEN}" +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +source ${PDO_HOME}/bin/lib/common.sh +check_python_version + +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=${PDO_HOSTNAME} +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/hfmodels/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/hfmodels/scripts/gs_stop.sh + ${PDO_HOME}/contracts/hfmodels/scripts/ss_stop.sh +} + +trap cleanup EXIT + +# ----------------------------------------------------------------- +# Start the guardian service and the storage service +# ----------------------------------------------------------------- +try ${PDO_HOME}/contracts/hfmodels/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/hfmodels/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. Token configured so that asset can be used at most twice. +try ex_token_issuer create ${OPTS} --contract token.test1.token_issuer +try hfmodel_token mint_hf_tokens ${OPTS} --contract token.test1.token_object \ + --hf_auth_token ${HF_AUTH_TOKEN} \ + --hf_endpoint_url "https://api-inference.huggingface.co/models/openai-community/gpt2" \ + --fixed_model_params '{"parameters": {"use_cache": false, "temperature": 0.10}}' \ + --user_inputs_schema '{"type": "object", "properties": {"inputs": {"type": "string"}}, "required": ["inputs"]}' \ + --payload_type "json" \ + --hfmodel_usage_info "this is this gpt2 opensource model available on Hugging Face, used for test purposes" \ + --max_use_count 2 + +yell get model info from the token object +try hfmodel_token get_modelinfo ${OPTS} --contract token.test1.token_object.token_1 \ + +yell use the hf model with user inputs, this is the first usage prior to transfer +try hfmodel_token use_hfmodel ${OPTS} --contract token.test1.token_object.token_1 \ + --user_inputs '{"inputs": "Can you please let us know more details about yourself ?"}' + +yell transfer the token to token_holder1 , only one more use permitted by the new owner +try hfmodel_token transfer ${OPTS} --contract token.test1.token_object.token_1 \ + --new-owner token_holder1 + +yell new owner uses the model, this is the second and last usage of the asset +try hfmodel_token use_hfmodel ${OPTS} --contract token.test1.token_object.token_1 \ + --user_inputs '{"inputs": "Do you know me ?"}' --identity token_holder1 + +yell new owner attempts 3rd usage, and this should fail +try hfmodel_token use_hfmodel ${OPTS} --contract token.test1.token_object.token_1 \ + --user_inputs '{"inputs": "Am I lucky today ?"}' --identity token_holder1 + +exit diff --git a/private-data-objects b/private-data-objects index 6a30f64..1fb32e8 160000 --- a/private-data-objects +++ b/private-data-objects @@ -1 +1 @@ -Subproject commit 6a30f6426883c22760dd9bd5eef452e9db5b3692 +Subproject commit 1fb32e8748cd94874851a6aee4d8ab952773d97b From dc390451a951b0fedaa93bfc3545dacf0c964b13 Mon Sep 17 00:00:00 2001 From: Prakash Narayana Moorthy Date: Mon, 8 Jul 2024 16:42:08 +0000 Subject: [PATCH 5/5] Basic Documentation for the hugging face contract family Signed-off-by: Prakash Narayana Moorthy --- hfmodels-contract/README.md | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 hfmodels-contract/README.md diff --git a/hfmodels-contract/README.md b/hfmodels-contract/README.md new file mode 100644 index 0000000..b221e36 --- /dev/null +++ b/hfmodels-contract/README.md @@ -0,0 +1,61 @@ + + +# PDO Contracts for tokenization and policy based access of Hugging Face models # + +This directory contains a Private Data Objects contract family for +creating a confidentiality preserving policy-wrapper around the usage +of (possibly private) machine learning (ML) models hosted on Hugging Face. +(`https://huggingface.co/models`). + + +## Problem Statement and Solution Overview +The majority of models shared via Hugging Face today are open-source. +For models that are private, or whose access need to be controlled, Hugging Face +provides the following options: + +1. The model repository and its deployment could be kept entirely private +(`https://huggingface.co/docs/hub/en/repositories-getting-started#creating-a-repository`) +in which case only the model owner (personal model) or members of the organization +(organization model) can see and access the deployment. + +2. `Gated Models` (`https://huggingface.co/docs/hub/models-gated`). Under gated models, prospective +users must provide basic information about themselves, and in addition should provide additional +information set by the model owner. Once approved by the model owner, the approved user gets access +to the model including the repository. + +However the above currently available solutions do not permit a model owner to set fine-grained polices +for access control of models; whose raw-bytes must otherwise be kept confidential. In this PoC, we provide +a solution for how policy-controlled access to "private" models hosted on Hugging Face can be provided +to any third-party whose use-case suffices to the use the model under the terms of the policy. At a high-level, +the solution works as follows: + +1. The model owner deploys the model as a private/gated model on Hugging Face. It is assumed that the model is available +for inference via Serverless API (`https://huggingface.co/docs/api-inference/index`) given the model owner's authentication tokens. (We haven't evaluated the PoC for Hugging Face managed inference endpoints (`https://huggingface.co/docs/inference-endpoints/index`) ) + +2. The model owner deploys PDO Token Object smart contract that encodes policies for usage of the model via Serverless Inference APIs. Currently, the PoC builds a generic token object which can be configured with the following information a. model owner authentication token b. REST API URL. Both of these are considered confidential information, and never exposed outside the PDO contract. The rest of the information is available for a prospective user of the asset c. Fixed Model parameters d. User input schema e. model/usage description f. max use count, referring to the maximum number of times model use capability packages can be obtained from the token object. + +3. The model owner transfers ownership of the PDO token object to a new user. New user submits model use requests to the token +object, obtains capability packages, and submits to the guardian web-server that acts as a bridge between the PDO token object, and the Hugging Face serverless API endpoint. Like the token object, the guardian web-server is generic. +We have programmed the guardian to check the schema of the user inputs against the schema set by the model owner as part of the the token object params; otherwise the guardian functionality is model agnostic. One limitation of this approach is that +if privacy preserving input pre-processing or output postprocessing needs to be carried out (which might be natural to expect +given that the problem assumes the model itself is private), the current PoC needs additional enhancements. + +4. The guardian invokes the REST API call to the serverless inference endpoint, obtains the result and returns back to the token owner. We have tested the PoC using some of the examples provided at `https://huggingface.co/docs/api-inference/detailed_parameters`. + +## Additional Details about the Solution + +At its core, the solution leverages the token-guardian protocol and its implementation contained within the [Exchange Contracts](../exchange-contract/README.md) family for policy-based use of high-value, possibly confidential assets. The current PoC does not employ TEEs for the guardian, however; for a more secure PoC, the token-guardian protocol permits usage of TEEs for the guardian web-server, and bi-directional attestation between the PDO token object and the guardian server. + +Additionally, this PoC conceptually is similar to the [inference-contract](../inference-contract/README.md) PoC, where we showed how to create PDO token objects/guardians for policy-based usage of high-value ML model deployed for inferencing via the OpenVINO model server. The major difference is that OpenVINO currently does not provide a hosted inferencing solution; so it is up to the model owner to deploy the OpenVINO model server that hosts the model; and also prove that the hosted solution respects usage of the inferencing data. In the current Hugging Face use-case, the model owner does not manage the inferncing infrastrture; rather simply relies on solutions provided by HuggingFace. A detailed comparison of privacy-preserving properties of the two PoCs for both the model owner; and the inferencing user is doable based on interest from the community. + + +## Testing the PoC + +The [test script](./test/script_test.sh) uses the open source `https://api-inference.huggingface.co/models/openai-community/gpt2` model available on Hugging Face via serverless APIs. In order to run the test script, please obtain a user access token +`https://huggingface.co/settings/tokens` that is necessary to make REST API calls to serverless API endpoints. Set the environment variable `HF_AUTH_TOKEN` with the token value. For the test, the assumption is that person who executes the test script owns the model; the token object will be configured with the person's token. The test will transfer token ownership to a fictitious `token_holder` and let the token_holder use the model subject to policies in the token object. + +We note that the test has not been integrated with the automated test suite for the contracts repo due to the dependency on the HF access token to run the test for this PoC. +