diff --git a/.circleci/config.yml b/.circleci/config.yml index ba46e35..f0d4abb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,11 +41,11 @@ parameters: # Define a job to be invoked later in a workflow. # See: https://circleci.com/docs/2.0/configuration-reference/#jobs jobs: - linux_ut: + cm_ut: # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor docker: - - image: secretflow/trustedflow-dev-ubuntu22.04:latest + - image: secretflow/trustflow-dev-ubuntu22.04:latest resource_class: 2xlarge+ shell: /bin/bash --login -eo pipefail # Add steps to the job @@ -62,7 +62,7 @@ jobs: cargo build cargo test - docker_image_publish: + cm_docker_image_publish: docker: - image: cimg/deploy:2023.06.1 resource_class: 2xlarge+ @@ -110,7 +110,49 @@ jobs: exit 1 ;; esac - + + cm_sdk_ut: + # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. + # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor + docker: + - image: secretflow/trustflow-dev-ubuntu22.04:latest + resource_class: large + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + shell: /bin/bash --login -eo pipefail + steps: + - checkout + - run: + name: "unit test" + command: | + source /root/miniconda3/etc/profile.d/conda.sh + conda create -n test python=3.10.14 -y + conda activate test + cd capsule-manager-sdk/python + export PYTHONPATH=.:$PYTHONPATH + pip install -r dev-requirements.txt + mkdir test-results + pytest --junitxml=test-results/junit-report.xml + - store_test_results: + path: capsule-manager-sdk/python/test-results + + cm_sdk_publish: + docker: + - image: secretflow/trustflow-dev-ubuntu22.04:latest + resource_class: large + shell: /bin/bash --login -eo pipefail + steps: + - checkout + - run: + name: "build package and publish" + command: | + source /root/miniconda3/etc/profile.d/conda.sh + conda create -n build python=3.10.14 -y + conda activate build + python3 -m pip install twine auditwheel patchelf + cd capsule-manager-sdk/python + python3 setup.py bdist_wheel && twine check dist/* + python3 -m twine upload -r pypi -u __token__ -p ${PYPI_TWINE_TOKEN} dist/*.whl # Invoke jobs via workflows # See: https://circleci.com/docs/2.0/configuration-reference/#workflows @@ -119,12 +161,22 @@ workflows: when: not: << pipeline.parameters.GHA_Action >> jobs: - - linux_ut - docker-image-publish-workflow: + - cm_ut + - cm_sdk_ut: + filters: + paths: + only: + - "capsule-manager-sdk/*" + cm-docker-image-publish-workflow: when: and: - - equal: ["docker_image_publish", << pipeline.parameters.GHA_Action >>] + - equal: ["cm_docker_image_publish", << pipeline.parameters.GHA_Action >>] - exits: << pipeline.parameters.GHA_Platform >> - exits: << pipeline.parameters.GHA_Version >> jobs: - - docker_image_publish + - cm_docker_image_publish + cm-sdk-publish-workflow: + when: + - equal: ["cm_sdk_publish", << pipeline.parameters.GHA_Action >>] + jobs: + - cm_sdk_publish \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_template.yaml b/.github/ISSUE_TEMPLATE/issue_template.yaml index 0015380..4ec9b50 100644 --- a/.github/ISSUE_TEMPLATE/issue_template.yaml +++ b/.github/ISSUE_TEMPLATE/issue_template.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: TrustedFlow Issue Template +name: TrustFlow Issue Template description: Thank you for reporting the issue! body: - type: dropdown @@ -34,7 +34,7 @@ body: id: source attributes: label: Source - description: TrustedFlow installed from + description: TrustFlow installed from options: - binary - source diff --git a/.rustfmt.toml b/.rustfmt.toml index f5fb264..6febba6 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -6,4 +6,4 @@ edition = "2021" # Line endings will be converted to \n. -newline_style = "Unix" +newline_style = "Unix" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 484ebb9..359fb46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,7 +445,7 @@ dependencies = [ "thiserror", "tokio", "tonic", - "trustedflow-attestation-rs", + "trustflow-attestation-rs", "uuid", ] @@ -922,7 +922,7 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "grpc-as" -version = "0.4.0" +version = "0.1.0" dependencies = [ "capsule_manager", "log", @@ -1059,6 +1059,19 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-as" +version = "0.1.0" +dependencies = [ + "axum 0.7.5", + "capsule_manager", + "log", + "openssl", + "openssl-sys", + "sdc_apis", + "tokio", +] + [[package]] name = "http-body" version = "0.4.6" @@ -3374,21 +3387,21 @@ dependencies = [ ] [[package]] -name = "trustedflow-attestation-rs" -version = "0.1.2-dev240313" +name = "trustflow-attestation-rs" +version = "0.3.1-dev20240726" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e57409d294b2d06f01cf7cf599f75401a29069098395265001099b4850ba9a" +checksum = "ee72c217eb6b1a3ec173bd064e68816a79c3e5c3d60cb46c1952db351bdf7108" dependencies = [ "sdc_apis", "serde_json", - "trustedflow-attestation-sys", + "trustflow-attestation-sys", ] [[package]] -name = "trustedflow-attestation-sys" -version = "0.1.1-dev240313" +name = "trustflow-attestation-sys" +version = "0.3.1-dev20240726" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12422e5133fe869c71ae87dcbf661837a21c273ed032ce1a08bd6153dbe82112" +checksum = "d6eeaf66ac6c3e73d2ec6bf06f1b96fef6ebb8cef03a2cac7939b3a1f54a411d" [[package]] name = "try-lock" diff --git a/Cargo.toml b/Cargo.toml index 58779e1..e202775 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] -members = [ - "bin/grpc-as", +members = [ + "bin/grpc-as", + "bin/http-as", "capsule-manager" ] diff --git a/bin/grpc-as/Cargo.toml b/bin/grpc-as/Cargo.toml index 7142ce5..60a4cab 100644 --- a/bin/grpc-as/Cargo.toml +++ b/bin/grpc-as/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "grpc-as" -version = "0.4.0" +version = "0.1.0" edition = "2021" [features] @@ -13,4 +13,4 @@ tonic = { version = "0.9.2", features = ["tls"] } log = "0.4" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } openssl-sys = "0.9" -openssl = { version = "0.10"} +openssl = { version = "0.10"} \ No newline at end of file diff --git a/bin/grpc-as/src/main.rs b/bin/grpc-as/src/main.rs index e03e9ce..4c1739c 100644 --- a/bin/grpc-as/src/main.rs +++ b/bin/grpc-as/src/main.rs @@ -122,6 +122,9 @@ async fn main() -> Result<(), Box> { let mut client_ca_pem = fs::read_to_string(&path).unwrap().as_bytes().to_vec(); client_pem_vec.append(&mut client_ca_pem); } + "read client ca pem {:?}", + std::str::from_utf8(&client_pem_vec)? + ); let client_ca_cert = tonic::transport::Certificate::from_pem(client_pem_vec); let tls_config = tonic::transport::ServerTlsConfig::new() diff --git a/bin/http-as/Cargo.toml b/bin/http-as/Cargo.toml new file mode 100644 index 0000000..b0f93bf --- /dev/null +++ b/bin/http-as/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "http-as" +version = "0.1.0" +edition = "2021" + +[features] +production = [] + +[dependencies] +axum = "0.7.5" +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +capsule_manager = {path = "../../capsule-manager"} +sdc_apis = "0.2.1-dev20240222" +openssl-sys = "0.9" +openssl = { version = "0.10"} +log = "0.4" \ No newline at end of file diff --git a/bin/http-as/src/main.rs b/bin/http-as/src/main.rs new file mode 100644 index 0000000..e3f950c --- /dev/null +++ b/bin/http-as/src/main.rs @@ -0,0 +1,434 @@ +use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; + +use capsule_manager::{ + common::constants, + config, + server::{self, CapsuleManagerService}, + utils::{self, tool}, +}; +use sdc_apis::secretflowapis::v2::{ + sdc::capsule_manager::{ + EncryptedRequest, EncryptedResponse, GetRaCertRequest, GetRaCertResponse, + }, + Status, +}; + +use capsule_manager::storage::in_memory_storage::InMemoryStorage; +use capsule_manager::storage::sql_storage::SqlStoreEngineBuilder; +use capsule_manager::storage::storage_engine::StorageEngine; +use std::{fs, time::Instant}; + +#[tokio::main(worker_threads = 16)] +async fn main() -> Result<(), Box> { + // Parse whole args with clap + let mut cfg = config::Config::new(); + // set mode according to cfg + if cfg!(feature = "production") { + cfg.mode = Some("production".to_owned()); + } else { + cfg.mode = Some("simulation".to_owned()); + } + log::info!("config {:#?}", cfg); + // init log + utils::log::init_log( + &cfg.log_config.log_dir.unwrap(), + &cfg.log_config.log_level.unwrap(), + cfg.log_config.enable_console_logger.unwrap(), + ); + + let (cm_cert, cm_private_key) = if cfg.enable_inject_cm_key.unwrap() { + let cm_cert: Vec = fs::read_to_string(cfg.cm_cert_path.as_ref().unwrap()) + .unwrap() + .into(); + let cm_private_key: Vec = fs::read_to_string(cfg.cm_private_key_path.as_ref().unwrap()) + .unwrap() + .into(); + (cm_cert, cm_private_key) + } else { + // get public-private key pair + let rsa = openssl::rsa::Rsa::generate(constants::RSA_BIT_LEN)?; + let key_pair = openssl::pkey::PKey::from_rsa(rsa)?; + let cert = crate::utils::crypto::create_cert( + &key_pair, + constants::X509NAME.iter(), + constants::CERT_DAYS, + )?; + let cert_pem = cert.to_pem()?; + let prikey_pem = key_pair.private_key_to_pem_pkcs8()?; + (cert_pem, prikey_pem) + }; + + // get backend storage client + let storage_engine: std::sync::Arc = match cfg + .storage_config + .storage_backend + .as_ref() + .unwrap() + .as_str() + { + "inmemory" => std::sync::Arc::new(InMemoryStorage::new()), + "mysql" => { + // use SHA256(SHA256(private key)) as seal key + let seal_key = tool::sha256(tool::sha256(cm_private_key.as_slice()).as_slice()); + std::sync::Arc::new( + SqlStoreEngineBuilder::new() + .db_url(cfg.storage_config.db_url.as_ref().expect("miss db url")) + .password(cfg.storage_config.password.as_ref()) + .seal_key(&seal_key[0..16]) + .build() + .await + .expect("connect to mysql failed"), + ) + } + _ => panic!("unsupport storage engine"), + }; + + let capsule_manager_service = server::CapsuleManagerImpl::new( + storage_engine.clone(), + &cfg.mode.as_ref().unwrap().as_str(), + cm_cert.as_ref(), + cm_private_key.as_ref(), + ) + .expect("capsule_manager init error"); + + let app = Router::new() + .route("/api/v1/ra_report/get", post(get_ra_cert)) + .route("/api/v1/data_keys/create", post(create_data_keys)) + .route("/api/v1/data_keys/get", post(get_data_keys)) + .route("/api/v1/data_key/delete", post(delete_data_key)) + .route("/api/v1/export_data_key/get", post(get_export_data_key)) + .route("/api/v1/cert/register", post(register_cert)) + .route("/api/v1/data_policy/create", post(create_data_policy)) + .route("/api/v1/data_policy/list", post(list_data_policy)) + .route("/api/v1/data_rule/add", post(add_data_rule)) + .route("/api/v1/data_policy/delete", post(delete_data_policy)) + .route("/api/v1/data_rule/delete", post(delete_data_rule)) + .route( + "/api/v1/result_data_key/create", + post(create_result_data_key), + ) + .with_state(capsule_manager_service); + let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", cfg.port.unwrap())) + .await + .unwrap(); + log::info!( + "Server run at: {:?} mode {:?}", + format!("0.0.0.0:{}", cfg.port.unwrap()), + cfg.mode + ); + axum::serve(listener, app).await?; + + Ok(()) +} + +async fn get_ra_cert( + State(capsule_manager_service): State, + Json(ra_cert_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service.get_ra_cert(&ra_cert_request).await { + Ok(response) => response, + Err(e) => GetRaCertResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + attestation_report: None, + cert: "".to_owned(), + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|get_ra_cert|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +async fn create_data_keys( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .create_data_keys(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|create_data_keys|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +async fn get_data_keys( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .get_data_keys(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|get_data_keys|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +async fn delete_data_key( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .delete_data_key(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|delete_data_key|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +async fn get_export_data_key( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .get_export_data_key(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|get_export_data_key|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +// warning: the rpc interface only take effect for party cert, +// doesn't take effect for certs derived from the party cert +async fn register_cert( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .register_cert(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|register_cert|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +async fn create_data_policy( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .create_data_policy(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|create_data_policy|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +async fn list_data_policy( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .list_data_policy(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|list_data_policy|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +async fn add_data_rule( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .add_data_rule(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|add_data_rule|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +async fn delete_data_policy( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .delete_data_policy(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|delete_data_policy|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +async fn delete_data_rule( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .delete_data_rule(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|delete_data_rule|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} + +async fn create_result_data_key( + State(capsule_manager_service): State, + Json(encrypted_request): Json, +) -> Result, (StatusCode, String)> { + let now = Instant::now(); + let reply = match capsule_manager_service + .create_result_data_key_impl(&encrypted_request) + .await + { + Ok(response) => response, + Err(e) => EncryptedResponse { + status: Some(Status { + code: e.errcode(), + message: e.to_string(), + details: vec![], + }), + message: None, + }, + }; + let elapsed = now.elapsed(); + if let Some(ref status) = reply.status { + log::info!(target: "monitor", "|create_result_data_key|{:?}|{:?}|{:?}", if status.code == 0 {"Y"} else {"N"}, elapsed.as_millis(), status.message); + } + Ok(Json(reply)) +} diff --git a/capsule-manager-sdk/.gitignore b/capsule-manager-sdk/.gitignore new file mode 100644 index 0000000..fcbd4ac --- /dev/null +++ b/capsule-manager-sdk/.gitignore @@ -0,0 +1,8 @@ +build +dist +*.egg-info +.vscode +**/__pycache__ +testdata +target +bazel-* \ No newline at end of file diff --git a/capsule-manager-sdk/README.md b/capsule-manager-sdk/README.md new file mode 100644 index 0000000..705478b --- /dev/null +++ b/capsule-manager-sdk/README.md @@ -0,0 +1,348 @@ +[![CircleCI](https://dl.circleci.com/status-badge/img/gh/secretflow/capsule-manager-sdk/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/secretflow/capsule-manager-sdk/tree/main) + +# CapsuleManager SDK + +CapsuleManager sdk offers several apis to access CapsuleManager Service, which is designed to manage metadata of user data and authorization information. + +## Features + +There are two ways to use CapsuleManager SDK: + +- If you will use sdk in source code, then you can just call functions defined in file python/sdc/capsule_manager_frame.py +- If you will use sdk in terminal, then you can use three commands:cms, cms_util, cms_config + +## Quick Start + +### Glossary + +- CMS: Capsule Manager Service +- Party: the user of Capsule Manager SDK +- Resource: usually refers to data +- Data Policy: some rules that restrict access to data, usually related to permissions + +### Install + +```bash +docker run -it --name capsule-manager-sdk --network=host secretflow/trustflow-release-ubuntu22.04:latest bash + +conda create -n capsule-manager-sdk python=3.10 -y +conda activate capsule-manager-sdk +pip install capsule-manager-sdk +``` + +### Use sdk in source code + +You can just call functions defined in file python/sdc/capsule_manager_frame.py. The function is as follows: + +- get_public_key: get CMS public key +- register_cert: register cert of Party in CMS +- register_data_keys: upload data keys of some Resources to CMS +- get_data_policys: get Data Policy of some Resources from CMS +- register_data_policy: upload Data Policy of some Resources to CMS +- delete_data_policy: delete Data Policy of some Resources to CMS +- add_data_rule: for one Data Policy, add rules for it +- delete_data_rule: for one Data Policy, delete rules for it +- get_export_data_key_b64: get base64 encoded data key for data generated from multiple datas which belong to different Partys, usually involving different Party's approval +- delete_data_key: delete data key of a specific Resource from CMS + +example: + +```bash +from sdc.capsule_manager_frame import CapsuleManagerFrame + +auth_frame = CapsuleManagerFrame( + "127.0.0.1:8888", + "sim" + None, + None, +) +public_key_pem = auth_frame.get_public_key() +print(public_key_pem) +``` + +For more examples please see file python/tests/test_capsule_manager.py + +### Use sdk in terminal + +There are three commands in terminal, the commands are following: + +- cms: according to the config file, call functions in file python/sdc/capsule_manager_frame.py. +- cms_config: help generate the config file which will be used in cms command +- cms_util: offer several convenient subcommands to use + +#### Introduction to command cms + +Command cms is the main command, it includes several subcommands which are following: + +```bash +cms --help +``` + +```bash +Usage: cms [OPTIONS] COMMAND [ARGS]... + +Options: + --config-file TEXT the config path + --help Show this message and exit. + +Commands: + add-data-rule add data rule for a specific... + delete-data-key delete the data key of a... + delete-data-policy delete data policy of a... + delete-data-rule delete data rule for a... + get-data-policys get data policy of the party... + get-export-data-key get the data key of export... + get-public-key get the pem format of public... + register-cert upload the cert of party... + register-data-keys upload data_keys of several... + register-data-policy upload data policy of a... + +``` + +If you want to know what subcommands or parameters are supported, just use --help + +```bash +# view supported subcommands +cms --help +# view supported parameters +cms --config-file=cli/cms/cli.yaml delete-data-rule --help +``` + +- config-file: the path of config file, we will explain it in the cms_config section +- commands: each command call one corresponding function in file python/sdc/capsule_manager_frame.py. I believe you can distinguish them from their names, for example, command get-public-key is calling function get_public_key + + ```bash + cms --config-file=cli/cms/cli.yaml get-public-key + # this will print public-key + + ``` + +### Introduction to the config file + +There are three parts in the config file python/cli/cli-template.yaml. + +- main section: it will be used to instantiate class CapsuleManagerFrame defined in file python/sdc/capsule_manager_frame.py + + ```bash + host: "127.0.0.1" + tee_plat: "sim" + tee_constraints: + mr_plat: null + mr_boot: null + mr_ta: null + mr_signer: null + root_ca_file: null + private_key_file: null + # List[str], cert chain file + cert_chain_file: null + ``` + +- common section: the common config of function part + + ```bash + common: + # str + party_id: "alice" + # List[str], cert chain file + cert_pems_file: null + # str + scheme: "RSA" + # file contains private key + private_key_file: null + ``` + +- function section: each section corresponds to a function call. for example, the fuction create_data_keys. As you can see, the configuration corresponds to the function parameters one-to-one.(of course, there are some function parameters in the common section) + + ```bash + # function defination + def create_data_keys( + self, + owner_party_id: str, + data_keys: List[dict], + cert_pems: List[bytes] = None, + private_key: Union[bytes, str, rsa.RSAPrivateKey] = None, + ): + + # the function part of the config file + register_data_keys: + data_keys: + - + # (required) str + resource_uri: + # (required) str + data_key_b64: + + ``` + +After the above explanation, you should understand the design concept of this configuration file, but there are two points to note: + +1. If the content of a configuration field is too long, it will be changed to read from a file. for example, the RSA key pair: + + ```bash + root_ca_file: null + private_key_file: null + cert_chain_file: null + ``` + +2. If the type of the content of a configuration field cannot be represented by a string, it will be changed to be represented by a string. for example, the type of data key is bytes, we will base64 encode it + + ```bash + # str + data_key_b64: + ``` + +so, How to modify the configuration file by cms_config command? + +### Introduction to command cms_config + +Command cms_config help modify the config file python/cli/cli-template.yaml which will be used in cms command + +Command cms_config is the main command, it includes several subcommands which are following: + +```bash +cms_config --help +``` + +```bash +Usage: cms_config [OPTIONS] COMMAND [ARGS]... + +Options: + --config-file TEXT config file path + --help Show this message and exit. + +Commands: + add-data-rule + common + create-data-policy + delete-data-policy + delete-data-rule + get-data-keys + get-data-policys + init +``` + +If you want to know what subcommands or parameters are supported, just use --help + +```bash +# view supported subcommands +cms_config --help +# view supported parameters +cms_config --config-file=cli/cms/cli.yaml init --help +``` + +Since cms_config modifies the config file and the config file has three sections, so the corresponding cms_config has three types of subcommands. + +- init: modify the main section of config file + +```bash +cms_config --config-file=cli/cms/cli.yaml init +``` + +- common: modify the common section of config file + +```bash +cms_config --config-file=cli/cms/cli.yaml common +``` + +- fuctuion: modify the fuction section of config file. for example, delete-data-rule + +```bash +cms_config --config-file=cli/cms/cli.yaml delete-data-rule +``` + +Please note that some parameters cannot be modified via the command cms_config, this is because we follow two principles: + +1. if the parameter type is list, it cannot be modified through the command line because [click](https://click.palletsprojects.com/en/8.1.x/) does not support nested lists. + +2. if the parameter content is too long, we do not support passing it in through the command line. + +### Introduction to command cms_util + +Command cms_util offers several convenient subcommands to use + +```bash +cms_util --help +``` + +```bash +Usage: cms_util [OPTIONS] COMMAND [ARGS]... + +Options: + --help Show this message and exit. + +Commands: + decrypt-file decrypt file using data key + decrypt-file-inplace decrypt file inplace using data key, it will... + encrypt-file encrypt file using data key + encrypt-file-inplace encrypt file inplace using data key, it will... + generate-data-key-b64 generate the base64 encode data key + generate-party-id generate the party id according to the certificate + generate-rsa-keypair generate rsa key pair (private_key, cert_chain) + generate-vote-result generate vote result json from... + sign-vote-request generate the vote request with signature when... + voter-sign generate voter signature when exporting the... +``` + +If you want to know what subcommands or parameters are supported, just use --help + +```bash +# view supported subcommands +cms_util --help +# view supported parameters +cms_config decrypt-file --help +``` + +for command cms_util, just use it. for example + +```bash +cms_util generate-data-key-b64 +# output +emK2Imaz9f6nZNWO2hBjdA== +``` + +For most functions, you can tell what they do by their names. +For a small number of functions that are difficult to understand, here is a detailed description. + +- generate-data-key-b64: generate data key and encode it with base64 + +- generate-party-id: generate the identifier of party based on its certificate + +- merge-cert-chain-files: merge multiple certificate files into a certificate chain file. Note that the order of the certificates is important. The last certificate is the CA. + +- sign-vote-request: when exporting data, data participants are required to vote whether to agree to the data export. This function is used to sign the vote request. +A template vote-request-template.yaml is provided to config vote request. + +- voter-sign: voter APPROVE the exporting vote request and sign the vote. A template voter-template.yaml is provided to config voter sign. + +The design idea of python/cli/vote-request-template.yaml and python/cli/voter-template.yaml is consistent with the previous file python/cli/cli-template.yaml and is not difficult to understand. + +```bash +vote_request: + # (required) str, vote type, should be "TEE_DOWNLOAD" when export data keys for tee tasks' encrypted result + type: "TEE_DOWNLOAD" + # (required) int, vote approved threshold + approved_threshold: + # (required) str, vote approved action, shoule be "tee/download,xxxx_uuid", replace "xxxx_uuid" with tee task's result data_uuid + approved_action: "tee/download,xxxx_uuid" + # (required) List[str], cert chain files, the order is [cert, mid_ca_cert, root_ca_cert] + # file num can be 1 if the cert is self-signed + cert_chain_file: + # (required) str, file contains private key + private_key_file: +``` + +```bash +# (required) str, vote request signature +vote_request_signature: +# (required) str, APPROVE/REJECT +action: "APPROVE" +# (required) List[str], cert chain files, the order is [cert, mid_ca_cert, root_ca_cert] +# file num can be 1 if the cert is self-signed +cert_chain_file: +# (required) str, file contains voter's private key +private_key_file: +``` + +## License + +This project is licensed under the [Apache License](LICENSE) \ No newline at end of file diff --git a/capsule-manager-sdk/python/.gitignore b/capsule-manager-sdk/python/.gitignore new file mode 100644 index 0000000..01d9b61 --- /dev/null +++ b/capsule-manager-sdk/python/.gitignore @@ -0,0 +1,7 @@ +.pytest_cache +test-results +*.so +wheelhouse +cli/*/* +.coverage +junit-report.xml diff --git a/capsule-manager-sdk/python/MANIFEST.in b/capsule-manager-sdk/python/MANIFEST.in new file mode 100644 index 0000000..e69de29 diff --git a/capsule-manager-sdk/python/cli/__init__.py b/capsule-manager-sdk/python/cli/__init__.py new file mode 100644 index 0000000..7832e73 --- /dev/null +++ b/capsule-manager-sdk/python/cli/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. diff --git a/capsule-manager-sdk/python/cli/cli-template.yaml b/capsule-manager-sdk/python/cli/cli-template.yaml new file mode 100644 index 0000000..b4810d5 --- /dev/null +++ b/capsule-manager-sdk/python/cli/cli-template.yaml @@ -0,0 +1,131 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. + +# (required) str, capsule-manager's ip:port +host: "127.0.0.1:8888" +# (required) str, capsule-manager's tee platform type, sim/sgx/tdx/csv +tee_plat: "sim" +# (optional) capsule-manager's identity constraints +tee_constraints: + # (optional) str, The measurement of TEE implement internal stuff + mr_plat: null + # (optional) str, The measurement of TEE instance boot time stuff + mr_boot: null + # (optional) str, The static measurement of trust application when loading the code + mr_ta: null + # (optional) str, The measurement or other identity of the trust application signer + mr_signer: null + +# (optional) str, root ca cert path +root_ca_file: null +# (optional) str, sdk's private key path +private_key_file: null +# (optional) List[str], sdk's cert chain path +cert_chain_file: null + +common: + # (required) str, should be generated from cert + party_id: "alice" + # (required) List[str], cert chain files, the order is [cert, mid_ca_cert, root_ca_cert] + # file num can be 1 if the cert is self-signed + cert_pems_file: null + # (required) str, keep it be "RSA" here + scheme: "RSA" + # (required) str, file contains private key + private_key_file: null + + +register_data_keys: + data_keys: + - + # (required) str + resource_uri: + # (required) str + data_key_b64: + +get_data_policys: + # (required) str + scope: + +register_data_policy: + # (required) str + scope: + # (required) str + data_uuid: + rules: + - + # (required) str + rule_id: + # (required) List[str] + grantee_party_ids: + # (required) List[str] + columns: + # (optional) List[str] + global_constraints: + # (required) List[dict] + op_constraints: + - + # (required) str + op_name: + # (optional) List[str] + constraints: + +delete_data_policy: + # (required) str + scope: + # (required) str + data_uuid: + +add_data_rule: + # (required) str + scope: + # (required) str + data_uuid: + # (required) dict + rule: + # (required) str + rule_id: + # (required) List[str] + grantee_party_ids: + # (required) List[str] + columns: + # (optional) List[str] + global_constraints: + # (required) List[dict] + op_constraints: + - + # (required) str + op_name: + # (optional) List[str] + constraints: + +delete_data_rule: + # (required) str + scope: + # (required) str + data_uuid: + # (required) str + rule_id: + +get_export_data_key_b64: + # (required) str + party_id: + # (required) str + resource_uri: + # (required) str + data_export_certificate_file: + +delete_data_key: + # (required) str + resource_uri: \ No newline at end of file diff --git a/capsule-manager-sdk/python/cli/cms.py b/capsule-manager-sdk/python/cli/cms.py new file mode 100644 index 0000000..455dc8b --- /dev/null +++ b/capsule-manager-sdk/python/cli/cms.py @@ -0,0 +1,293 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 base64 +import os +from typing import List + +import click +from google.protobuf import json_format +from sdc import capsule_manager_frame +from sdc.util import file + +current_work_dir = os.path.dirname(__file__) +CONFIG_FILE = current_work_dir + "/cms/cli.yaml" + + +def read_rsa_keypair(cfg) -> tuple: + if ( + "cert_pems_file" in cfg + and "private_key_file" in cfg + and cfg["private_key_file"] is not None + and cfg["cert_pems_file"] is not None + ): + cert_pems_str = list() + for filename in cfg["cert_pems_file"]: + cert_pems_str.append(file.read_file(filename, "r")) + private_key_str = file.read_file(cfg["private_key_file"], "r") + if isinstance(cert_pems_str, List): + return ( + [cert.encode("utf-8") for cert in cert_pems_str], + private_key_str.encode("utf-8"), + ) + return None, None + + +@click.group() +@click.option( + "--config-file", type=click.STRING, default=CONFIG_FILE, help="the config path" +) +@click.pass_context +def cms(ctx, config_file): + global CONFIG_FILE + CONFIG_FILE = config_file + config = file.read_yaml_file(CONFIG_FILE) + tee_constraints = config["tee_constraints"] + mr_plat = ( + tee_constraints["mr_plat"] if tee_constraints["mr_plat"] is not None else "" + ) + mr_boot = ( + tee_constraints["mr_boot"] if tee_constraints["mr_boot"] is not None else "" + ) + mr_ta = tee_constraints["mr_ta"] if tee_constraints["mr_ta"] is not None else "" + mr_signer = ( + tee_constraints["mr_signer"] if tee_constraints["mr_signer"] is not None else "" + ) + + if ( + config["root_ca_file"] is not None + and config["private_key_file"] is not None + and config["cert_chain_file"] is not None + ): + root_ca: bytes = file.read_file(config["root_ca_file"], "r").encode("utf-8") + private_key: bytes = file.read_file(config["private_key_file"], "r").encode( + "utf-8" + ) + cert_chain: bytes = file.read_file(config["cert_chain_file"], "r").encode( + "utf-8" + ) + + ctx.obj = capsule_manager_frame.CapsuleManagerFrame( + config["host"], + config["tee_plat"], + capsule_manager_frame.TeeConstraints(mr_plat, mr_boot, mr_ta, mr_signer), + capsule_manager_frame.CredentialsConf(root_ca, private_key, cert_chain), + ) + else: + ctx.obj = capsule_manager_frame.CapsuleManagerFrame( + config["host"], + config["tee_plat"], + capsule_manager_frame.TeeConstraints(mr_plat, mr_boot, mr_ta, mr_signer), + None, + ) + + +@cms.command() +@click.pass_context +def get_public_key(ctx): + """ + get the pem format of public key of CapsuleManager + """ + public_key_pem = ctx.obj.get_public_key() + print(public_key_pem) + + +@cms.command() +@click.pass_context +def register_cert(ctx): + """ + upload the cert of party using the sdk to CapsuleManager + """ + config = file.read_yaml_file(CONFIG_FILE) + config = config["common"] + cert_pems, private_key = read_rsa_keypair(config) + + ctx.obj.register_cert(config["party_id"], cert_pems, config["scheme"], private_key) + + +@cms.command() +@click.pass_context +def register_data_keys(ctx): + """ + upload data_keys of several resource_uris to CapsuleManager + """ + config = file.read_yaml_file(CONFIG_FILE) + common = config["common"] + ownered = config["register_data_keys"] + cert_pems, private_key = read_rsa_keypair(common) + + data_keys = ownered["data_keys"] + + # check data_key_b64 format + for data_key in data_keys: + data_key_b64 = data_key.get("data_key_b64") + try: + base64.b64decode(data_key_b64, validate=True) + except (ValueError, base64.binascii.Error): + raise ValueError( + f"The provided data_key_b64: {data_key_b64} is not a valid base64 encoded string" + ) + + ctx.obj.create_data_keys( + common["party_id"], + data_keys, + cert_pems, + private_key, + ) + + +@cms.command() +@click.pass_context +def get_data_policys(ctx): + """ + get data policy of the party using sdk from CapsuleManager + """ + config = file.read_yaml_file(CONFIG_FILE) + common = config["common"] + ownered = config["get_data_policys"] + cert_pems, private_key = read_rsa_keypair(common) + + result = ctx.obj.get_data_policys( + common["party_id"], ownered["scope"], cert_pems, private_key + ) + for policy in result: + print(json_format.MessageToJson(policy)) + + +@cms.command() +@click.pass_context +def register_data_policy(ctx): + """ + upload data policy of a specific data to CapsuleManager + """ + config = file.read_yaml_file(CONFIG_FILE) + common = config["common"] + ownered = config["register_data_policy"] + cert_pems, private_key = read_rsa_keypair(common) + + ctx.obj.create_data_policy( + common["party_id"], + ownered["scope"], + ownered["data_uuid"], + ownered["rules"], + cert_pems, + private_key, + ) + + +@cms.command() +@click.pass_context +def delete_data_policy(ctx): + """ + delete data policy of a specific data to CapsuleManager + """ + config = file.read_yaml_file(CONFIG_FILE) + common = config["common"] + ownered = config["delete_data_policy"] + cert_pems, private_key = read_rsa_keypair(common) + + ctx.obj.delete_data_policy( + common["party_id"], + ownered["scope"], + ownered["data_uuid"], + cert_pems, + private_key, + ) + + +@cms.command() +@click.pass_context +def add_data_rule(ctx): + """ + add data rule for a specific policy to CapsuleManager + """ + config = file.read_yaml_file(CONFIG_FILE) + common = config["common"] + ownered = config["add_data_rule"] + cert_pems, private_key = read_rsa_keypair(common) + + ctx.obj.add_data_rule( + common["party_id"], + ownered["scope"], + ownered["data_uuid"], + ownered["rule"], + cert_pems, + private_key, + ) + + +@cms.command() +@click.pass_context +def delete_data_rule(ctx): + """ + delete data rule for a specific policy to CapsuleManager + """ + config = file.read_yaml_file(CONFIG_FILE) + common = config["common"] + ownered = config["delete_data_rule"] + cert_pems, private_key = read_rsa_keypair(common) + + ctx.obj.delete_data_rule( + common["party_id"], + ownered["scope"], + ownered["data_uuid"], + ownered["rule_id"], + cert_pems, + private_key, + ) + + +@cms.command() +@click.pass_context +def get_export_data_key_b64(ctx): + """ + get the base64 encoded data key of export data(often is generated from origin + datas of multiply differernt partys) from CapsuleManager + """ + config = file.read_yaml_file(CONFIG_FILE) + common = config["common"] + ownered = config["get_export_data_key_b64"] + cert_pems, private_key = read_rsa_keypair(common) + + data_key = ctx.obj.get_export_data_key_b64( + common["party_id"], + ownered["resource_uri"], + file.read_file(ownered["data_export_certificate_file"], "r"), + cert_pems, + private_key, + ) + print(data_key) + + +@cms.command() +@click.pass_context +def delete_data_key(ctx): + """ + delete the data key of a specific resource_uri from CapsuleManager + """ + config = file.read_yaml_file(CONFIG_FILE) + common = config["common"] + ownered = config["delete_data_key"] + cert_pems, private_key = read_rsa_keypair(common) + + ctx.obj.delete_data_key( + common["party_id"], + ownered["resource_uri"], + cert_pems, + private_key, + ) + + +if __name__ == "__main__": + cms() diff --git a/capsule-manager-sdk/python/cli/cms_config.py b/capsule-manager-sdk/python/cli/cms_config.py new file mode 100644 index 0000000..67c8c8c --- /dev/null +++ b/capsule-manager-sdk/python/cli/cms_config.py @@ -0,0 +1,255 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 stat + +import click +from ruamel.yaml import YAML + +current_work_dir = os.path.dirname(__file__) +CONFIG_FILE = current_work_dir + "/cms/cli.yaml" + +# init yaml +yaml = YAML() +yaml.preserve_quotes = True +yaml.indent(mapping=2, sequence=4, offset=2) +yaml.width = 40960 + + +def restore_config(cfg: dict): + with open(CONFIG_FILE + ".bak", "w") as f: + yaml.dump(cfg, f) + os.remove(CONFIG_FILE) + os.rename(CONFIG_FILE + ".bak", CONFIG_FILE) + os.chmod(CONFIG_FILE, stat.S_IRWXU + stat.S_IRWXG + stat.S_IRWXO) + + +def set_dict_value(cfg: dict, key, value): + if key and value: + cfg[key] = value + + +@click.group() +@click.pass_context +@click.option( + "--config-file", type=click.STRING, default=CONFIG_FILE, help="config file path" +) +def cms_config(ctx, config_file): + global CONFIG_FILE + CONFIG_FILE = config_file + with open(CONFIG_FILE) as f: + config = yaml.load(f) + ctx.obj = config + + +@cms_config.command() +@click.option("--host", type=click.STRING, help="the host of capsule manager") +@click.option( + "--tee-plat", + type=click.STRING, + help="the platform of tee, should be sim/sgx/tdx/csv", +) +@click.option( + "--mr-plat", + type=click.STRING, + help="the measurement of TEE implement internal stuff", +) +@click.option( + "--mr-boot", + type=click.STRING, + help="the measurement of TEE instance boot time stuff", +) +@click.option( + "--mr-ta", + type=click.STRING, + help="the static measurement of trust application when loading the code", +) +@click.option( + "--mr-signer", + type=click.STRING, + help="the measurement or other identity of the trust application signer", +) +@click.option( + "--root-ca-file", type=click.STRING, help="the root CA of capsule manager" +) +@click.option( + "--private-key-file", + type=click.STRING, + help="the private key of the party using capsule manager sdk", +) +@click.option( + "--cert-chain-file", + type=click.STRING, + multiple=True, + help="the cert chain of the party using capsule manager sdk", +) +@click.pass_context +def init( + ctx, + host, + tee_plat, + mr_plat, + mr_boot, + mr_ta, + mr_signer, + root_ca_file, + private_key_file, + cert_chain_file, +): + set_dict_value(ctx.obj, "host", host) + set_dict_value(ctx.obj, "tee_plat", tee_plat) + + if "tee_constraints" not in ctx.obj: + ctx.obj["tee_constraints"] = {} + set_dict_value(ctx.obj["tee_constraints"], "mr_plat", mr_plat) + set_dict_value(ctx.obj["tee_constraints"], "mr_boot", mr_boot) + set_dict_value(ctx.obj["tee_constraints"], "mr_ta", mr_ta) + set_dict_value(ctx.obj["tee_constraints"], "mr_signer", mr_signer) + + set_dict_value(ctx.obj, "root_ca_file", root_ca_file) + set_dict_value(ctx.obj, "private_key_file", private_key_file) + set_dict_value(ctx.obj, "cert_chain_file", cert_chain_file) + + restore_config(ctx.obj) + + +@cms_config.command() +@click.option( + "--party-id", type=click.STRING, help="the party using capsule manager sdk" +) +@click.option( + "--cert-pems-file", + type=click.STRING, + multiple=True, + help="the cert chain id of the party using capsule manager sdk", +) +@click.option("--scheme", type=click.STRING, help="the scheme of key, RSA or SM2") +@click.option( + "--private-key-file", + type=click.STRING, + help="the private key of the party using capsule manager sdk", +) +@click.pass_context +def common(ctx, party_id, cert_pems_file, scheme, private_key_file): + if "common" not in ctx.obj: + ctx.obj["common"] = {} + + set_dict_value(ctx.obj["common"], "party_id", party_id) + set_dict_value(ctx.obj["common"], "cert_pems_file", cert_pems_file) + set_dict_value(ctx.obj["common"], "scheme", value=scheme) + set_dict_value(ctx.obj["common"], "private_key_file", private_key_file) + + restore_config(ctx.obj) + + +@cms_config.command() +@click.option( + "--scope", + type=click.STRING, + default="default", + help="corresponding to the scope in the policy", +) +@click.pass_context +def get_data_policys(ctx, scope): + if "get_data_policys" not in ctx.obj: + ctx.obj["get_data_policys"] = {} + + set_dict_value(ctx.obj["get_data_policys"], "scope", scope) + + restore_config(ctx.obj) + + +@cms_config.command() +@click.option( + "--scope", + type=click.STRING, + default="default", + help="corresponding to the scope in the policy,", +) +@click.option("--data-uuid", type=click.STRING, help="the identifier of data") +@click.pass_context +def create_data_policy(ctx, scope, data_uuid): + if "create_data_policy" not in ctx.obj: + ctx.obj["create_data_policy"] = {} + + set_dict_value(ctx.obj["create_data_policy"], "scope", scope) + set_dict_value(ctx.obj["create_data_policy"], "data_uuid", data_uuid) + + restore_config(ctx.obj) + + +@cms_config.command() +@click.option( + "--scope", + type=click.STRING, + default="default", + help="corresponding to the scope in the policy,", +) +@click.option("--data-uuid", type=click.STRING, help="the identifier of data") +@click.pass_context +def delete_data_policy(ctx, scope, data_uuid): + if "delete_data_policy" not in ctx.obj: + ctx.obj["delete_data_policy"] = {} + + set_dict_value(ctx.obj["delete_data_policy"], "scope", scope) + set_dict_value(ctx.obj["delete_data_policy"], "data_uuid", data_uuid) + + restore_config(ctx.obj) + + +@cms_config.command() +@click.option( + "--scope", + type=click.STRING, + default="default", + help="corresponding to the scope in the policy,", +) +@click.option("--data-uuid", type=click.STRING, help="the identifier of data") +@click.option("--rule-id", type=click.STRING, help="the identifier of rule") +@click.pass_context +def add_data_rule(ctx, scope, data_uuid, rule_id): + if "add_data_rule" not in ctx.obj: + ctx.obj["add_data_rule"] = {} + + set_dict_value(ctx.obj["add_data_rule"], "scope", scope) + set_dict_value(ctx.obj["add_data_rule"], "data_uuid", data_uuid) + set_dict_value(ctx.obj["add_data_rule"], "rule_id", rule_id) + + restore_config(ctx.obj) + + +@cms_config.command() +@click.option( + "--scope", + type=click.STRING, + default="default", + help="corresponding to the scope in the policy", +) +@click.option("--data-uuid", type=click.STRING, help="the identifier of data") +@click.option("--rule-id", type=click.STRING, help="the identifier of rule") +@click.pass_context +def delete_data_rule(ctx, scope, data_uuid, rule_id): + if "delete_data_rule" not in ctx.obj: + ctx.obj["delete_data_rule"] = {} + + set_dict_value(ctx.obj["delete_data_rule"], "scope", scope) + set_dict_value(ctx.obj["delete_data_rule"], "data_uuid", data_uuid) + set_dict_value(ctx.obj["delete_data_rule"], "rule_id", rule_id) + + restore_config(ctx.obj) + + +if __name__ == "__main__": + cms_config() diff --git a/capsule-manager-sdk/python/cli/cms_util.py b/capsule-manager-sdk/python/cli/cms_util.py new file mode 100644 index 0000000..6973b3d --- /dev/null +++ b/capsule-manager-sdk/python/cli/cms_util.py @@ -0,0 +1,330 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 base64 +import json + +import click +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from sdc.crypto import asymm +from sdc.util import crypto, file, tool + +VOTE_REQUEST = "vote_request" +CERT_CHAIN_FILE = "cert_chain_file" +CERT_CHAIN = "cert_chain" +BODY = "body" +PRIVATE_KEY_FILE = "private_key_file" +VOTE_REQUEST_SIGNATURE = "vote_request_signature" +VOTER_SIGNATURE = "voter_signature" +VOTE_INVITE = "vote_invite" + + +@click.group() +def cms_util(): + pass + + +@cms_util.command() +@click.option( + "--private-key-file", + type=click.STRING, + help="file path for storing private key", +) +@click.option( + "--cert-file", + type=click.STRING, + help="file path for storing cert chain which is list", +) +def generate_rsa_keypair(private_key_file, cert_file): + """ + generate rsa key pair (private_key, cert_chain) + """ + ( + pri_key_pem, + cert_pems, + ) = crypto.generate_rsa_keypair() + if private_key_file: + file.write_file(private_key_file, "w", pri_key_pem.decode("utf-8")) + if cert_file: + file.write_file(cert_file, "w", cert_pems[0].decode("utf-8")) + + +@cms_util.command() +@click.option( + "--cert-file", + type=click.STRING, + multiple=True, + required=True, + help="a list of cert files, the order is important, the last file is CA cert", +) +def generate_party_id(cert_file): + """ + generate the party id according to the certificate + """ + cert_chain = [] + for cert in cert_file: + cert_chain.append(file.read_file(cert, "r")) + print(tool.generate_party_id_from_cert(cert_chain[-1].encode("utf-8"))) + + +@cms_util.command() +@click.option( + "--bit-len", type=click.INT, default=128, help="the bit len of secret key" +) +def generate_data_key_b64(bit_len): + """ + generate the base64 encode data key + """ + data_key = AESGCM.generate_key(bit_len) + print(base64.b64encode(data_key).decode("utf-8")) + + +@cms_util.command() +@click.option( + "--source-file", + type=click.STRING, + required=True, + help="the source file which needs to be encrypted", +) +@click.option( + "--dest-file", + type=click.STRING, + help="the dest file which stores encrypted data", +) +@click.option( + "--data-key-b64", + type=click.UNPROCESSED, + required=True, + help="the secret key used to encrypt data in base64 encode format", +) +def encrypt_file(source_file, dest_file, data_key_b64): + """ + encrypt file using data key + """ + data_key: bytes = base64.b64decode(data_key_b64) + if dest_file is None or len(dest_file) == 0: + dest_file = source_file + ".enc" + crypto.encrypt_file(source_file, dest_file, data_key) + + +@cms_util.command() +@click.option( + "--source-file", + type=click.STRING, + required=True, + help="the source file which needs to be decrypted", +) +@click.option( + "--dest-file", + type=click.STRING, + help="the dest file which stores decrypted data", +) +@click.option( + "--data-key-b64", + type=click.UNPROCESSED, + required=True, + help="the secret key used to decrypt data in base64 encode format", +) +def decrypt_file(source_file, dest_file, data_key_b64): + """ + decrypt file using data key + """ + data_key: bytes = base64.b64decode(data_key_b64) + if dest_file is None or len(dest_file) == 0: + dest_file = source_file + ".dec" + crypto.decrypt_file(source_file, dest_file, data_key) + + +@cms_util.command() +@click.option( + "--file", + type=click.STRING, + required=True, + help="the file which needs to be encrypted", +) +@click.option( + "--data-key-b64", + type=click.UNPROCESSED, + required=True, + help="the secret key used to decrypt data in base64 encode format", +) +def encrypt_file_inplace(file, data_key_b64): + """ + encrypt file inplace using data key, it will change origin file + """ + data_key: bytes = base64.b64decode(data_key_b64) + crypto.encrypt_file_inplace(file, data_key) + + +@cms_util.command() +@click.option( + "--file", + type=click.STRING, + required=True, + help="the file which needs to be decrypted", +) +@click.option( + "--data-key-b64", + type=click.UNPROCESSED, + required=True, + help="the secret key used to decrypt data in base64 encode format", +) +def decrypt_file_inplace(file, data_key_b64): + """ + decrypt file inplace using data key, it will change origin file + """ + data_key: bytes = base64.b64decode(data_key_b64) + crypto.decrypt_file_inplace(file, data_key) + + +@cms_util.command() +@click.option( + "--vote-request-file", + type=click.STRING, + required=True, + help="the original vote request file", +) +@click.option( + "--signed-vote-request-file", + type=click.STRING, + required=True, + help="the signed vote request file", +) +def sign_vote_request(vote_request_file, signed_vote_request_file): + """ + generate the vote request with signature when exporting the result data + """ + config = file.read_yaml_file(vote_request_file) + vote_request = config[VOTE_REQUEST] + signed_vote_request = dict() + + cert_chain = list() + if vote_request.get(CERT_CHAIN_FILE) is not None: + for filename in vote_request.pop(CERT_CHAIN_FILE): + cert_chain.append(file.read_file(filename, "r")) + signed_vote_request[CERT_CHAIN] = cert_chain + + if vote_request.get(PRIVATE_KEY_FILE) is not None: + private_key = file.read_file(vote_request.pop(PRIVATE_KEY_FILE), "r").encode( + "utf-8" + ) + vote_body_str = json.dumps(vote_request) + vote_body_b64 = base64.b64encode(vote_body_str.encode("utf-8")).decode("utf-8") + signed_vote_request[BODY] = vote_body_b64 + + # vote_request_signature + signature_b64 = base64.b64encode( + asymm.RsaSigner(private_key, "RS256") + .update(vote_body_b64.encode("utf-8")) + .sign() + ).decode("utf-8") + signed_vote_request[VOTE_REQUEST_SIGNATURE] = signature_b64 + + file.write_yaml_file(signed_vote_request, signed_vote_request_file) + + +@cms_util.command() +@click.option( + "--voter-file", type=click.STRING, required=True, help="the voter's config file" +) +@click.option( + "--signed-voter-file", + type=click.STRING, + required=True, + help="the voter's signed file", +) +def voter_sign(voter_file, signed_voter_file): + """ + generate voter signature when exporting the result data + """ + voter = file.read_yaml_file(voter_file) + voter_signed = dict() + + cert_chain = list() + if voter.get(CERT_CHAIN_FILE) is not None: + for filename in voter.pop(CERT_CHAIN_FILE): + cert_chain.append(file.read_file(filename, "r")) + voter_signed[CERT_CHAIN] = cert_chain + + # get private key + private_key = file.read_file(voter.pop(PRIVATE_KEY_FILE), "r").encode("utf-8") + # get requester's sign + request_sign = voter.pop(VOTE_REQUEST_SIGNATURE) + + body_str = json.dumps(voter) + body_b64 = base64.b64encode(body_str.encode("utf-8")).decode("utf-8") + voter_signed[BODY] = body_b64 + + signature_b64 = base64.b64encode( + asymm.RsaSigner(private_key, "RS256") + .update(body_b64.encode("utf-8")) + .update(request_sign.encode("utf-8")) + .sign() + ).decode("utf-8") + + voter_signed[VOTER_SIGNATURE] = signature_b64 + file.write_yaml_file(voter_signed, signed_voter_file) + + +@cms_util.command() +@click.option( + "--signed-vote-request-file", + type=click.STRING, + required=True, + help="the signed vote request file", +) +@click.option( + "--signed-voter-files", + type=click.STRING, + required=True, + multiple=True, + help="the voter's signed files", +) +@click.option( + "--vote-result-file", + type=click.STRING, + required=True, + help="the file to store voting result", +) +def generate_vote_result( + signed_vote_request_file, signed_voter_files, vote_result_file +): + """ + generate vote result json from signed-vote-request-file and signed-voter-files + """ + vote_result_config = dict() + + vote_request = dict() + vote_request_config = file.read_yaml_file(signed_vote_request_file) + vote_request[CERT_CHAIN] = vote_request_config[CERT_CHAIN] + vote_request[BODY] = vote_request_config[BODY] + vote_request[VOTE_REQUEST_SIGNATURE] = vote_request_config[VOTE_REQUEST_SIGNATURE] + + vote_invite = list() + for signed_voter_file in signed_voter_files: + signed_voter_config = file.read_yaml_file(signed_voter_file) + vote_invite_item = dict() + vote_invite_item[CERT_CHAIN] = signed_voter_config[CERT_CHAIN] + vote_invite_item[BODY] = signed_voter_config[BODY] + vote_invite_item[VOTER_SIGNATURE] = signed_voter_config[VOTER_SIGNATURE] + vote_invite.append(vote_invite_item) + + vote_result_config[VOTE_REQUEST] = vote_request + vote_result_config[VOTE_INVITE] = vote_invite + + file.write_file(vote_result_file, "w", json.dumps(vote_result_config)) + + +if __name__ == "__main__": + cms_util() diff --git a/capsule-manager-sdk/python/cli/vote-request-template.yaml b/capsule-manager-sdk/python/cli/vote-request-template.yaml new file mode 100644 index 0000000..1495163 --- /dev/null +++ b/capsule-manager-sdk/python/cli/vote-request-template.yaml @@ -0,0 +1,26 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. + +vote_request: + # (required) str, vote type, should be "TEE_DOWNLOAD" when export data keys for tee tasks' encrypted result + type: "TEE_DOWNLOAD" + # (required) int, vote approved threshold + approved_threshold: + # (required) str, vote approved action, shoule be "tee/download,xxxx_uuid", replace "xxxx_uuid" with tee task's result data_uuid + approved_action: "tee/download,xxxx_uuid" + # (required) List[str], cert chain files, the order is [cert, mid_ca_cert, root_ca_cert] + # file num can be 1 if the cert is self-signed + cert_chain_file: + # (required) str, file contains private key + private_key_file: diff --git a/capsule-manager-sdk/python/cli/voter-template.yaml b/capsule-manager-sdk/python/cli/voter-template.yaml new file mode 100644 index 0000000..e6ba356 --- /dev/null +++ b/capsule-manager-sdk/python/cli/voter-template.yaml @@ -0,0 +1,23 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. + +# (required) str, vote request signature +vote_request_signature: +# (required) str, APPROVE/REJECT +action: "APPROVE" +# (required) List[str], cert chain files, the order is [cert, mid_ca_cert, root_ca_cert] +# file num can be 1 if the cert is self-signed +cert_chain_file: +# (required) str, file contains voter's private key +private_key_file: \ No newline at end of file diff --git a/capsule-manager-sdk/python/dev-requirements.txt b/capsule-manager-sdk/python/dev-requirements.txt new file mode 100644 index 0000000..4d465ed --- /dev/null +++ b/capsule-manager-sdk/python/dev-requirements.txt @@ -0,0 +1,5 @@ +-r requirements.txt +coverage +pytest +pytest-cov +twine \ No newline at end of file diff --git a/capsule-manager-sdk/python/env.sh b/capsule-manager-sdk/python/env.sh new file mode 100644 index 0000000..f335550 --- /dev/null +++ b/capsule-manager-sdk/python/env.sh @@ -0,0 +1,35 @@ +# +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. +# +#!/bin/bash +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +image=secretflow/trustflow-dev-ubuntu22.04:latest +DOCKER=docker +project=capsule-manager-sdk + +if [[ $1 == 'enter' ]]; then + $DOCKER exec -it ${project}-build-ubuntu-$(whoami) bash +else +$DOCKER run --name ${project}-build-ubuntu-$(whoami) -td \ + --rm -it --entrypoint /bin/bash \ + --net host \ + -v $DIR:$DIR \ + -v /root/${USER}-${project}-bazel-cache-test:/root/.cache/bazel \ + -w $DIR \ + --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ + --cap-add=NET_ADMIN \ + --privileged=true \ + ${image} +fi \ No newline at end of file diff --git a/capsule-manager-sdk/python/pytest.ini b/capsule-manager-sdk/python/pytest.ini new file mode 100644 index 0000000..e1aa727 --- /dev/null +++ b/capsule-manager-sdk/python/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +python_files = tests/*test_*.py \ No newline at end of file diff --git a/capsule-manager-sdk/python/requirements.txt b/capsule-manager-sdk/python/requirements.txt new file mode 100644 index 0000000..548b666 --- /dev/null +++ b/capsule-manager-sdk/python/requirements.txt @@ -0,0 +1,5 @@ +cryptography>=41.0.2 +grpcio==1.62.1 +ruamel.yaml==0.18.6 +click==8.1.7 +trustflow-verification==0.3.0.dev20240801 \ No newline at end of file diff --git a/capsule-manager-sdk/python/sdc/.gitignore b/capsule-manager-sdk/python/sdc/.gitignore new file mode 100644 index 0000000..7951405 --- /dev/null +++ b/capsule-manager-sdk/python/sdc/.gitignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/capsule-manager-sdk/python/sdc/__init__.py b/capsule-manager-sdk/python/sdc/__init__.py new file mode 100644 index 0000000..7832e73 --- /dev/null +++ b/capsule-manager-sdk/python/sdc/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. diff --git a/capsule-manager-sdk/python/sdc/capsule_manager_frame.py b/capsule-manager-sdk/python/sdc/capsule_manager_frame.py new file mode 100644 index 0000000..07a2f4a --- /dev/null +++ b/capsule-manager-sdk/python/sdc/capsule_manager_frame.py @@ -0,0 +1,647 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 base64 +from dataclasses import dataclass +from typing import List, Union + +import grpc +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from google.protobuf import json_format, message +from sdc.crypto import asymm, symm +from sdc.error import CapsuleManagerError +from sdc.util import crypto, tool +from secretflowapis.v2.sdc import jwt_pb2, ual_pb2 +from secretflowapis.v2.sdc.capsule_manager import ( + capsule_manager_pb2, + capsule_manager_pb2_grpc, +) + +NONCE_SIZE_IN_SIZE = 32 + +# tee plat types +TEE_PLAT_SIM = "sim" +TEE_PLAT_SGX = "sgx" +TEE_PLAT_TDX = "tdx" +TEE_PLAT_CSV = "csv" + +# tee plat types in UAL Protobuf +UAL_TEE_PLAT_SGX = "SGX_DCAP" +UAL_TEE_PLAT_TDX = "TDX" +UAL_TEE_PLAT_CSV = "CSV" + +RESOURCE_URI = "resource_uri" +DATA_KEY_B64 = "data_key_b64" +RULE_ID = "rule_id" +GRANTEE_PARTY_IDS = "grantee_party_ids" +COLUMNS = "columns" +GLOBAL_CONSTRAINTS = "global_constraints" +OP_CONSTRAINS = "op_constraints" +OP_NAME = "op_name" +CONSTRAINTS = "constraints" + + +@dataclass +class CredentialsConf: + root_ca: bytes + private_key: bytes + cert_chain: bytes + + +@dataclass +class TeeConstraints: + mr_plat: str + mr_boot: str + mr_ta: str + mr_signer: str + + +class CapsuleManagerFrame(object): + def __init__( + self, + host: str, + tee_plat: str, + tee_constraints: TeeConstraints, + conf: CredentialsConf, + ): + """CapsuleManager client + + Args: + host: CapsuleManager endpoint + tee_plat: Tee platform, sim/sgx/tdx/csv + tee_constraints: CapsuleManager's measurement constraints + """ + self.tee_plat = tee_plat + self.tee_constraints = tee_constraints + if conf is None: + channel = grpc.insecure_channel(host) + else: + credentials = grpc.ssl_channel_credentials( + root_certificates=conf.root_ca, + private_key=conf.private_key, + certificate_chain=conf.cert_chain, + ) + channel = grpc.secure_channel(host, credentials) + + self.stub = capsule_manager_pb2_grpc.CapsuleManagerStub(channel) + + @staticmethod + def create_encrypted_request( + request: message.Message, + public_key: bytes, + private_key: Union[bytes, str, rsa.RSAPrivateKey], + cert_pems: List[bytes] = None, + ) -> capsule_manager_pb2.EncryptedRequest: + """encrypt request + + Args: + request: the item will be encrypted + public_key: the public key of capsule manager, it will be used to encrypt data key + cert_pems: the cert chain of party, it will be used to verify signature and encrypt + private_key: the private key of party, it will be used to sign and decrypt + """ + jws = jwt_pb2.Jws() + jws_JoseHeader = jws.JoseHeader() + jws_JoseHeader.alg = "RS256" + if cert_pems is not None: + cert_chain = [ + # has padding + base64.standard_b64encode(crypto.convert_pem_to_der(cert_pem)).decode( + "utf-8" + ) + for cert_pem in cert_pems + ] + jws_JoseHeader.x5c.extend(cert_chain) + jws.protected_header = tool.encode_base64( + json_format.MessageToJson(jws_JoseHeader).encode("utf-8") + ) + jws.payload = tool.encode_base64( + json_format.MessageToJson(request).encode("utf-8") + ) + + jws.signature = tool.encode_base64( + asymm.RsaSigner(private_key, "RS256") + .update(jws.protected_header.encode("utf-8")) + .update(b".") + .update(jws.payload.encode("utf-8")) + .sign() + ) + + jwe = jwt_pb2.Jwe() + jwe_header = jwe.JoseHeader() + jwe_header.alg = "RSA-OAEP-256" + jwe_header.enc = "A128GCM" + jwe.protected_header = tool.encode_base64( + json_format.MessageToJson(jwe_header).encode("utf-8") + ) + + # generate temp data_key, it will be used to encrypt data + data_key = AESGCM.generate_key(bit_length=128) + # use public key of capsule manager to encrypt data key + jwe.encrypted_key = tool.encode_base64( + asymm.RsaEncryptor(public_key, "RSA-OAEP-256").encrypt(data_key) + ) + + nonce = crypto.gen_key(NONCE_SIZE_IN_SIZE) + jwe.iv = tool.encode_base64(nonce) + jwe.aad = "" + + (ciphertext, tag) = symm.AesGcmEncryptor(data_key, "A128GCM").encrypt( + json_format.MessageToJson(jws).encode("utf-8"), nonce, b"" + ) + jwe.ciphertext = tool.encode_base64(ciphertext) + jwe.tag = tool.encode_base64(tag) + + encrypted_request = capsule_manager_pb2.EncryptedRequest() + encrypted_request.message.CopyFrom(jwe) + encrypted_request.has_signature = True + return encrypted_request + + @staticmethod + def parse_from_encrypted_response( + response: capsule_manager_pb2.EncryptedResponse, + private_key: Union[bytes, str, rsa.RSAPrivateKey], + msg: message.Message, + ): + """decrypt request + + Args: + response: the item will be decrypted + private_key: the private key of party, it will be used to decrypt + """ + + jwe = response.message + jwe_header = jwe.JoseHeader() + json_format.Parse(tool.decode_base64(jwe.protected_header), jwe_header) + iv = tool.decode_base64(jwe.iv) + ciphertext = tool.decode_base64(jwe.ciphertext) + tag = tool.decode_base64(jwe.tag) + add = tool.decode_base64(jwe.aad) + + data_key = asymm.RsaDecryptor(private_key, jwe_header.alg).decrypt( + tool.decode_base64(jwe.encrypted_key) + ) + plain_text = symm.AesGcmDecryptor(data_key, jwe_header.enc).decrypt( + ciphertext, iv, add, tag + ) + json_format.Parse(plain_text, msg) + + def get_public_key(self) -> bytes: + """Get CapsuleManager public key""" + request = capsule_manager_pb2.GetRaCertRequest() + nonce_bytes = crypto.gen_key(32) + request.nonce = tool.to_hex(nonce_bytes) + response = self.stub.GetRaCert(request) + if response.status.code != 0: + raise CapsuleManagerError(response.status.code, response.status.message) + assert len(response.cert) != 0, "The CapsuleManager should have public key." + + if self.tee_plat != TEE_PLAT_SIM: + from trustflow.attestation.verification import verifier + + policy = ual_pb2.UnifiedAttestationPolicy() + rule = policy.main_attributes.add() + + user_data = crypto.sha256( + response.cert.encode("utf-8"), request.nonce.encode("utf-8") + ) + rule.hex_user_data = tool.to_hex(user_data) + + if self.tee_plat == TEE_PLAT_SGX: + rule.bool_debug_disabled = "true" + rule.str_tee_platform = UAL_TEE_PLAT_SGX + rule.hex_ta_measurement = self.tee_constraints.mr_ta + rule.hex_signer = self.tee_constraints.mr_signer + elif self.tee_plat == TEE_PLAT_TDX: + rule.bool_debug_disabled = "true" + rule.str_tee_platform = UAL_TEE_PLAT_TDX + rule.hex_platform_measurement = self.tee_constraints.mr_plat + rule.hex_boot_measurement = self.tee_constraints.mr_boot + rule.hex_ta_measurement = self.tee_constraints.mr_ta + elif self.tee_plat == TEE_PLAT_CSV: + rule.str_tee_platform = UAL_TEE_PLAT_CSV + rule.hex_boot_measurement = self.tee_constraints.mr_boot + else: + raise ValueError(f"Invalid TEE platform: {self.tee_plat}") + + report_json = json_format.MessageToJson( + response.attestation_report, including_default_value_fields=True + ) + policy_json = json_format.MessageToJson( + policy, including_default_value_fields=True + ) + verify_status = verifier.attestation_report_verify(report_json, policy_json) + if verify_status.code != 0: + raise RuntimeError( + f"attestation_report_verify failed. Code:{verify_status.code}, Message:{verify_status.message}, Details:{verify_status.details}." + ) + + cert = x509.load_pem_x509_certificate(response.cert.encode("utf-8")) + return cert.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + def register_cert( + self, + owner_party_id: str, + cert_pems: List[bytes], + scheme: str, + private_key: Union[bytes, str, rsa.RSAPrivateKey], + ): + """register cert + + Args: + owner_party_id: data owner + cert_pems: cert chain. cert_pems[0] is current cert + scheme: `RSA`, `SM2` + private_key: private key of party + + """ + request = capsule_manager_pb2.RegisterCertRequest() + request.owner_party_id = owner_party_id + request.certs.extend([cert_pem.decode("utf-8") for cert_pem in cert_pems]) + request.scheme = scheme + + encrypted_response = self.stub.RegisterCert( + self.create_encrypted_request( + request, self.get_public_key(), private_key, None + ) + ) + if encrypted_response.status.code != 0: + raise CapsuleManagerError( + encrypted_response.status.code, encrypted_response.status.message + ) + + def create_data_keys( + self, + owner_party_id: str, + data_keys: List[dict], + cert_pems: List[bytes] = None, + private_key: Union[bytes, str, rsa.RSAPrivateKey] = None, + ): + """create data keys + + Args: + owner_party_id: data owner + data_keys: list of data_key, data_key is a dict contains "resource_uri" and "data_key_b64" + cert_pems: cert chain of party + private_key: private key of party + + """ + request = capsule_manager_pb2.CreateDataKeysRequest() + request.owner_party_id = owner_party_id + + for data_key in data_keys: + request.data_keys.add( + resource_uri=data_key.get(RESOURCE_URI), + data_key_b64=data_key.get(DATA_KEY_B64), + ) + + if private_key is None: + # Generate temp RSA key-pair + (private_key, cert_pems) = crypto.generate_rsa_keypair() + + encrypted_response = self.stub.CreateDataKeys( + self.create_encrypted_request( + request, self.get_public_key(), private_key, cert_pems + ) + ) + if encrypted_response.status.code != 0: + raise CapsuleManagerError( + encrypted_response.status.code, encrypted_response.status.message + ) + + def get_data_policys( + self, + owner_party_id: str, + scope: str, + cert_pems: List[bytes] = None, + private_key: Union[bytes, str, rsa.RSAPrivateKey] = None, + ) -> List[capsule_manager_pb2.Policy]: + """create data policy + + Args: + owner_party_id: data policy's owner + scope: scope + cert_pems: cert chain of party + private_key: private key of party + + Returns: + List[capsule_manager_pb2.Policy]: the list of policy + """ + request = capsule_manager_pb2.ListDataPolicyRequest() + request.owner_party_id = owner_party_id + request.scope = scope + + if private_key is None: + # Generate temp RSA key-pair + (private_key, cert_pems) = crypto.generate_rsa_keypair() + + encrypted_response = self.stub.ListDataPolicy( + self.create_encrypted_request( + request, self.get_public_key(), private_key, cert_pems + ) + ) + if encrypted_response.status.code != 0: + raise CapsuleManagerError( + encrypted_response.status.code, encrypted_response.status.message + ) + # decrypt request + response = capsule_manager_pb2.ListDataPolicyResponse() + self.parse_from_encrypted_response(encrypted_response, private_key, response) + + return list(response.policies) + + def create_data_policy( + self, + owner_party_id: str, + scope: str, + data_uuid: str, + rules: List[dict], + cert_pems: List[bytes] = None, + private_key: Union[bytes, str, rsa.RSAPrivateKey] = None, + ): + """create data policy + + Args: + owner_party_id: data owner + scope: scope + data_uuid: data id + rules: list of rule, rule is a dict contains: + rule_id: id of the rule + grantee_party_ids: for every rule, the list of party ids being guanteed + columns: for every rule, specify which columns can be used, if this is a structued data + global_constraints: for every rule, gobal DSL decribed additional constraints + op_constraints: list of op_constraint, it has op_name and corresponding constraints + cert_pems: cert chain of party + private_key: private key of party + + """ + request = capsule_manager_pb2.CreateDataPolicyRequest() + request.owner_party_id = owner_party_id + request.scope = scope + request.policy.data_uuid = data_uuid + + for rule in rules: + rule_add = request.policy.rules.add() + rule_add.rule_id = rule.get(RULE_ID) + if rule.get(GRANTEE_PARTY_IDS) is not None: + rule_add.grantee_party_ids.extend(rule.get(GRANTEE_PARTY_IDS)) + if rule.get(COLUMNS) is not None: + rule_add.columns.extend(rule.get(COLUMNS)) + if rule.get(GLOBAL_CONSTRAINTS) is not None: + rule_add.global_constraints.extend(rule.get(GLOBAL_CONSTRAINTS)) + if rule.get(OP_CONSTRAINS) is not None: + for op_constraint in rule.get(OP_CONSTRAINS): + constraints = ( + op_constraint.get(CONSTRAINTS) + if op_constraint.get(CONSTRAINTS) is not None + else [] + ) + rule_add.op_constraints.add( + op_name=op_constraint.get(OP_NAME), constraints=constraints + ) + + if private_key is None: + # Generate temp RSA key-pair + (private_key, cert_pems) = crypto.generate_rsa_keypair() + + encrypted_response = self.stub.CreateDataPolicy( + self.create_encrypted_request( + request, self.get_public_key(), private_key, cert_pems + ) + ) + if encrypted_response.status.code != 0: + raise CapsuleManagerError( + encrypted_response.status.code, encrypted_response.status.message + ) + + def delete_data_policy( + self, + owner_party_id: str, + scope: str, + data_uuid: str, + cert_pems: List[bytes] = None, + private_key: Union[bytes, str, rsa.RSAPrivateKey] = None, + ): + """delete data policy + + Args: + owner_party_id: data owner + scope: scope + data_uuid: data id + cert_pems: cert chain of party + private_key: private key of party + + """ + request = capsule_manager_pb2.DeleteDataPolicyRequest() + request.owner_party_id = owner_party_id + request.scope = scope + request.data_uuid = data_uuid + + if private_key is None: + # Generate temp RSA key-pair + (private_key, cert_pems) = crypto.generate_rsa_keypair() + + encrypted_response = self.stub.DeleteDataPolicy( + self.create_encrypted_request( + request, self.get_public_key(), private_key, cert_pems + ) + ) + if encrypted_response.status.code != 0: + raise CapsuleManagerError( + encrypted_response.status.code, encrypted_response.status.message + ) + + def add_data_rule( + self, + owner_party_id: str, + scope: str, + data_uuid: str, + rule: dict, + cert_pems: List[bytes] = None, + private_key: Union[bytes, str, rsa.RSAPrivateKey] = None, + ): + """add data rule + + Args: + owner_party_id: data owner + scope: scope + data_uuid: data id + rule: rule is a dict contains: + rule_id: id of the rule + grantee_party_ids: for every rule, the list of party ids being guanteed + columns: for every rule, specify which columns can be used, if this is a structued data + global_constraints: for every rule, gobal DSL decribed additional constraints + op_constraints: list of op_constraint, it has op_name and corresponding constraints + cert_pems: cert chain of party + private_key: private key of party + + """ + request = capsule_manager_pb2.AddDataRuleRequest() + request.owner_party_id = owner_party_id + request.data_uuid = data_uuid + request.scope = scope + + request.rule.rule_id = rule.get(RULE_ID) + if rule.get(GRANTEE_PARTY_IDS) is not None: + request.rule.grantee_party_ids.extend(rule.get(GRANTEE_PARTY_IDS)) + if rule.get(COLUMNS) is not None: + request.rule.columns.extend(rule.get(COLUMNS)) + if rule.get(GLOBAL_CONSTRAINTS) is not None: + request.rule.global_constraints.extend(rule.get(GLOBAL_CONSTRAINTS)) + if rule.get(OP_CONSTRAINS) is not None: + for op_constraint in rule.get(OP_CONSTRAINS): + constraints = ( + op_constraint.get(CONSTRAINTS) + if op_constraint.get(CONSTRAINTS) is not None + else [] + ) + request.rule.op_constraints.add( + op_name=op_constraint.get(OP_NAME), constraints=constraints + ) + + if private_key is None: + # Generate temp RSA key-pair + (private_key, cert_pems) = crypto.generate_rsa_keypair() + + encrypted_response = self.stub.AddDataRule( + self.create_encrypted_request( + request, self.get_public_key(), private_key, cert_pems + ) + ) + if encrypted_response.status.code != 0: + raise CapsuleManagerError( + encrypted_response.status.code, encrypted_response.status.message + ) + + def delete_data_rule( + self, + owner_party_id: str, + scope: str, + data_uuid: str, + rule_id: str, + cert_pems: List[bytes] = None, + private_key: Union[bytes, str, rsa.RSAPrivateKey] = None, + ): + """delete data rule + + Args: + owner_party_id: data owner + scope: scope + data_uuid: data id + rule_id: identifier of rule + cert_pems: cert chain of party + private_key: private key of party + + """ + request = capsule_manager_pb2.DeleteDataRuleRequest() + request.owner_party_id = owner_party_id + request.scope = scope + request.data_uuid = data_uuid + request.rule_id = rule_id + + if private_key is None: + # Generate temp RSA key-pair + (private_key, cert_pems) = crypto.generate_rsa_keypair() + + encrypted_response = self.stub.DeleteDataRule( + self.create_encrypted_request( + request, self.get_public_key(), private_key, cert_pems + ) + ) + if encrypted_response.status.code != 0: + raise CapsuleManagerError( + encrypted_response.status.code, encrypted_response.status.message + ) + + def get_export_data_key_b64( + self, + request_party_id: str, + resource_uri: str, + data_export_certificate: str, + cert_pems: List[bytes] = None, + private_key: Union[bytes, str, rsa.RSAPrivateKey] = None, + ) -> str: + """get export base64 encoded data key + + Args: + request_party_id: the request owner + resource_uri: the identifier of resource + data_export_certificate: Data Export Certificate, json format + When the data request exporting party requests to obtain the decryption key + for accessing the data, they need to obtain the signatures of all the + original owners of the data, the request information, and the signature of + the original owner, which together constitute the data export certificate. + """ + request = capsule_manager_pb2.GetExportDataKeyRequest() + request.request_party_id = request_party_id + request.resource_uri = resource_uri + request.data_export_certificate = data_export_certificate + + if private_key is None: + # Generate temp RSA key-pair + (private_key, cert_pems) = crypto.generate_rsa_keypair() + + encrypted_response = self.stub.GetExportDataKey( + self.create_encrypted_request( + request, self.get_public_key(), private_key, cert_pems + ) + ) + if encrypted_response.status.code != 0: + raise CapsuleManagerError( + encrypted_response.status.code, encrypted_response.status.message + ) + # decrypt request + response = capsule_manager_pb2.GetExportDataKeyResponse() + self.parse_from_encrypted_response(encrypted_response, private_key, response) + return response.data_key.data_key_b64 + + def delete_data_key( + self, + owner_party_id: str, + resource_uri: str, + cert_pems: List[bytes] = None, + private_key: Union[bytes, str, rsa.RSAPrivateKey] = None, + ): + """delete data key + + Args: + owner_party_id: data owner + resource_uri: the resource uri corresponding to the data key + cert_pems: cert chain of party + private_key: private key of party + + """ + request = capsule_manager_pb2.DeleteDataKeyRequest() + request.owner_party_id = owner_party_id + request.resource_uri = resource_uri + + if private_key is None: + # Generate temp RSA key-pair + (private_key, cert_pems) = crypto.generate_rsa_keypair() + + encrypted_response = self.stub.DeleteDataKey( + self.create_encrypted_request( + request, self.get_public_key(), private_key, cert_pems + ) + ) + if encrypted_response.status.code != 0: + raise CapsuleManagerError( + encrypted_response.status.code, encrypted_response.status.message + ) diff --git a/capsule-manager-sdk/python/sdc/crypto/__init__.py b/capsule-manager-sdk/python/sdc/crypto/__init__.py new file mode 100644 index 0000000..7832e73 --- /dev/null +++ b/capsule-manager-sdk/python/sdc/crypto/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. diff --git a/capsule-manager-sdk/python/sdc/crypto/asymm.py b/capsule-manager-sdk/python/sdc/crypto/asymm.py new file mode 100644 index 0000000..583e259 --- /dev/null +++ b/capsule-manager-sdk/python/sdc/crypto/asymm.py @@ -0,0 +1,253 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 abc import ABC, abstractmethod +from typing import Union + +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa, utils +from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes + + +def load_public_key(data: bytes, format: str = "PEM") -> PublicKeyTypes: + if format == "PEM": + return serialization.load_pem_public_key(data) + elif format == "DER": + return serialization.load_der_public_key(data) + else: + raise RuntimeError(f"public key format {format} is not supported") + + +def load_private_key(data: bytes, format: str = "PEM") -> PublicKeyTypes: + if format == "PEM": + return serialization.load_pem_private_key(data, None) + elif format == "DER": + return serialization.load_der_private_key(data, None) + else: + raise RuntimeError(f"public key format {format} is not supported") + + +class Encryptor(ABC): + def __init__(self, name: str): + """init Encryptor + + Args: + name: encrypt method name + """ + self.name = name + + @abstractmethod + def encrypt(self, data: bytes) -> bytes: + pass + + def name(self) -> str: + return self.name + + +class Decryptor(ABC): + def __init__(self, name: str): + """init Decryptor + + Args: + name: decrypt method name + """ + self.name = name + + @abstractmethod + def decrypt(self, data: bytes) -> bytes: + pass + + def name(self) -> str: + return self.name + + +class Signer(ABC): + def __init__(self, name: str): + """init Signer + + Args: + name: signer method name + """ + self.name = name + + @abstractmethod + def update(self, data: bytes): + pass + + @abstractmethod + def sign(self) -> bytes: + pass + + def name(self) -> str: + return self.name + + +class Verifier(ABC): + def __init__(self, name: str): + """init Verifier + + Args: + name: verifier method name + """ + self.name = name + + @abstractmethod + def update(self, data: bytes): + pass + + @abstractmethod + def verify(self, signature: bytes) -> None: + pass + + def name(self) -> str: + return self.name + + +class RsaEncryptor(Encryptor): + def __init__( + self, + public_key: Union[bytes, str, rsa.RSAPublicKey], + name: str, + format: str = "PEM", + ): + super().__init__(name) + if isinstance(public_key, bytes): + self.public_key = load_public_key(public_key, format) + elif isinstance(public_key, str): + self.public_key = load_public_key(public_key.encode("utf-8"), format) + else: + self.public_key = public_key + + def encrypt(self, data: bytes) -> bytes: + if self.name == "RSA-OAEP-256": + return self.public_key.encrypt( + data, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None, + ), + ) + elif self.name == "RSA-OAEP": + return self.public_key.encrypt( + data, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None, + ), + ) + else: + raise RuntimeError(f"encrypted method {self.name} not support") + + +class RsaDecryptor(Decryptor): + def __init__( + self, + private_key: Union[bytes, str, rsa.RSAPrivateKey], + name: str, + format: str = "PEM", + ): + super().__init__(name) + if isinstance(private_key, bytes): + self.secret_key = load_private_key(private_key, format) + elif isinstance(private_key, str): + self.secret_key = load_private_key(private_key.encode("utf-8"), format) + else: + self.secret_key = private_key + + def decrypt(self, data: bytes) -> bytes: + if self.name == "RSA-OAEP-256": + return self.secret_key.decrypt( + data, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None, + ), + ) + elif self.name == "RSA-OAEP": + return self.secret_key.decrypt( + data, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA1()), + algorithm=hashes.SHA1(), + label=None, + ), + ) + else: + raise RuntimeError(f"decrypt method {self.name} not support") + + +class RsaVerifier(Verifier): + def __init__( + self, + public_key: Union[bytes, str, rsa.RSAPublicKey], + name: str, + format: str = "PEM", + ): + super().__init__(name) + if isinstance(public_key, bytes): + self.public_key = load_public_key(public_key, format) + elif isinstance(public_key, str): + self.public_key = load_public_key(public_key.encode("utf-8"), format) + else: + self.public_key = public_key + self.hasher = hashes.Hash(hashes.SHA256()) + + def update(self, data: bytes): + self.hasher.update(data) + return self + + def verify(self, signature: bytes) -> None: + if self.name == "RS256": + digest = self.hasher.finalize() + self.public_key.verify( + signature, + digest, + padding.PKCS1v15(), + utils.Prehashed(hashes.SHA256()), + ) + else: + raise RuntimeError(f"verifier method {self.name} not support") + + +class RsaSigner(Signer): + def __init__( + self, + private_key: Union[bytes, str, rsa.RSAPrivateKey], + name: str, + format: str = "PEM", + ): + super().__init__(name) + if isinstance(private_key, bytes): + self.secret_key = load_private_key(private_key, format) + elif isinstance(private_key, str): + self.secret_key = load_private_key(private_key.encode("utf-8"), format) + else: + self.secret_key = private_key + self.hasher = hashes.Hash(hashes.SHA256()) + + def update(self, data: bytes): + self.hasher.update(data) + return self + + def sign(self) -> bytes: + if self.name == "RS256": + digest = self.hasher.finalize() + return self.secret_key.sign( + digest, padding.PKCS1v15(), utils.Prehashed(hashes.SHA256()) + ) + else: + raise RuntimeError(f"signer method {self.name} not support") diff --git a/capsule-manager-sdk/python/sdc/crypto/symm.py b/capsule-manager-sdk/python/sdc/crypto/symm.py new file mode 100644 index 0000000..7425f56 --- /dev/null +++ b/capsule-manager-sdk/python/sdc/crypto/symm.py @@ -0,0 +1,88 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 abc import ABC, abstractmethod +from typing import Union + +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + + +class Encryptor(ABC): + def __init__(self, name: str): + """init Encryptor + + Args: + name: encrypt method name + """ + self.name = name + + @abstractmethod + def encrypt(self, data: bytes) -> bytes: + pass + + def name(self) -> str: + return self.name + + +class Decryptor(ABC): + def __init__(self, name: str): + """init Decryptor + + Args: + name: decrypt method name + """ + self.name = name + + @abstractmethod + def decrypt(self, data: bytes) -> bytes: + pass + + def name(self) -> str: + return self.name + + +class AesGcmEncryptor(Encryptor): + def __init__(self, secret_key: Union[bytes, str], name: str): + super().__init__(name) + if isinstance(secret_key, str): + self.secret_key = secret_key.encode("utf-8") + else: + self.secret_key = secret_key + + def encrypt(self, data: bytes, iv: bytes, aad: bytes) -> (bytes, bytes): + encryptor = Cipher( + algorithms.AES(self.secret_key), + modes.GCM(iv), + ).encryptor() + encryptor.authenticate_additional_data(aad) + ciphertext = encryptor.update(data) + encryptor.finalize() + + return (ciphertext, encryptor.tag) + + +class AesGcmDecryptor(Decryptor): + def __init__(self, secret_key: Union[bytes, str], name: str): + super().__init__(name) + if isinstance(secret_key, str): + self.secret_key = secret_key.encode("utf-8") + else: + self.secret_key = secret_key + + def decrypt(self, data: bytes, iv: bytes, aad: bytes, tag: bytes) -> bytes: + decryptor = Cipher( + algorithms.AES(self.secret_key), + modes.GCM(iv, tag), + ).decryptor() + decryptor.authenticate_additional_data(aad) + return decryptor.update(data) + decryptor.finalize() diff --git a/capsule-manager-sdk/python/sdc/error.py b/capsule-manager-sdk/python/sdc/error.py new file mode 100644 index 0000000..95f157b --- /dev/null +++ b/capsule-manager-sdk/python/sdc/error.py @@ -0,0 +1,33 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. + + +class CapsuleManagerError(Exception): + """Raises when CapsuleManager returns errors. + + Attributes: + code: An integer indicating exception error code. + message: A str indicating exception error message. + """ + + def __init__(self, code: int = None, message: str = None): + self.code = code + self.message = message + super().__init__(self.message) + + def __str__(self) -> str: + return ( + f"CapsuleManager server error code: {self.code}, " + f"error message: {self.message}" + ) diff --git a/capsule-manager-sdk/python/sdc/util/__init__.py b/capsule-manager-sdk/python/sdc/util/__init__.py new file mode 100644 index 0000000..7832e73 --- /dev/null +++ b/capsule-manager-sdk/python/sdc/util/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. diff --git a/capsule-manager-sdk/python/sdc/util/constants.py b/capsule-manager-sdk/python/sdc/util/constants.py new file mode 100644 index 0000000..6ac387e --- /dev/null +++ b/capsule-manager-sdk/python/sdc/util/constants.py @@ -0,0 +1,33 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. + +SEPARATOR = b"." + +Version = 1 +Schema = 1 +VersionBytes = 4 +SchemaBytes = 4 + +BlockBytes = 0x2000 +PacketCntBytes = 8 +BlockLenBytes = 4 + +IvBytes = 12 +MacBytes = 16 +ContentKeyBytes = 16 +IvFieldBytes = 32 +MacFieldBytes = 32 +IvLenBytes = 1 +MacLenBytes = 1 +Padding = 0 diff --git a/capsule-manager-sdk/python/sdc/util/crypto.py b/capsule-manager-sdk/python/sdc/util/crypto.py new file mode 100644 index 0000000..6481127 --- /dev/null +++ b/capsule-manager-sdk/python/sdc/util/crypto.py @@ -0,0 +1,248 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 datetime +import os +import secrets +from io import BufferedWriter +from typing import List + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, hmac, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID +from sdc.crypto import symm +from sdc.util import constants + + +def bytes2int(buf: bytes): + res = 0 + for index in range(len(buf)): + res = res << 8 | buf[len(buf) - index - 1] + return res + + +def hmac_sha256(key: bytes, *args: bytes) -> bytes: + h = hmac.HMAC(key, hashes.SHA256()) + assert ( + len(args) >= 1 + ), "At least one piece of data is involved in the calculation of hmac." + h.update(args[0]) + for arg in args[1:]: + h.update(constants.SEPARATOR) + h.update(arg) + return h.finalize() + + +def sha256(*args: bytes) -> bytes: + h = hashes.Hash(hashes.SHA256()) + assert ( + len(args) >= 1 + ), "At least one piece of data is involved in the calculation of hash." + h.update(args[0]) + for arg in args[1:]: + h.update(constants.SEPARATOR) + h.update(arg) + return h.finalize() + + +def gen_key(nbytes: int = 32) -> bytes: + return secrets.token_bytes(nbytes) + + +def decrypt_block(buf: bytes, data_key: bytes) -> bytes: + offset = 0 + # read iv + iv_len = bytes2int(buf[offset : offset + constants.IvLenBytes]) + offset += constants.IvLenBytes + iv = buf[offset : offset + iv_len] + offset += constants.IvFieldBytes + + # read mac + mac_len = bytes2int(buf[offset : offset + constants.MacLenBytes]) + offset += constants.MacLenBytes + mac = buf[offset : offset + mac_len] + offset += constants.MacFieldBytes + + # read data + data = buf[offset:] + if len(data_key) == 16: + return symm.AesGcmDecryptor(data_key, "A128GCM").decrypt(data, iv, b"", mac) + elif len(data_key) == 32: + return symm.AesGcmDecryptor(data_key, "A256GCM").decrypt(data, iv, b"", mac) + else: + return symm.AesGcmDecryptor(data_key, "Unknown").decrypt(data, iv, b"", mac) + + +def decrypt_file(source_path: str, dest_path: str, data_key: bytes): + assert source_path != dest_path, "source_path = dest_path" + in_ = open(source_path, "rb") + out_ = open(dest_path, "wb") + + # parse file header + file_len = os.path.getsize(source_path) + header_len = ( + constants.VersionBytes + + constants.SchemaBytes + + constants.PacketCntBytes + + constants.BlockLenBytes + ) + assert file_len > header_len, "File length is less than required header length" + + # skip version and schema + in_.seek(constants.VersionBytes + constants.SchemaBytes) + # read packet count + buf = in_.read(constants.PacketCntBytes) + packet_cnt = bytes2int(buf) + # read block len + buf = in_.read(constants.BlockLenBytes) + block_len = bytes2int(buf) + + assert ( + file_len - header_len >= (packet_cnt - 1) * block_len + ), "N - 1 Data block len is more than required file length" + assert ( + block_len * packet_cnt >= file_len - header_len + ), "N Data block len is less than required file length" + + for index in range(packet_cnt): + buf = in_.read(block_len) + data = decrypt_block(buf, data_key) + out_.write(data) + + out_.close() + in_.close() + + +def encrypt_block(buf: bytes, data_key: bytes, out_: BufferedWriter): + # iv + iv = gen_key(constants.IvBytes) + if len(data_key) == 16: + (ciphertext, mac) = symm.AesGcmEncryptor(data_key, "A128GCM").encrypt( + buf, iv, b"" + ) + elif len(data_key) == 32: + (ciphertext, mac) = symm.AesGcmEncryptor(data_key, "A256GCM").encrypt( + buf, iv, b"" + ) + else: + (ciphertext, mac) = symm.AesGcmEncryptor(data_key, "Unknown").encrypt( + buf, iv, b"" + ) + + # write block header + out_.write(constants.IvBytes.to_bytes(constants.IvLenBytes, "little")) + out_.write(iv) + out_.write(constants.Padding.to_bytes(constants.IvFieldBytes - len(iv), "little")) + out_.write(len(mac).to_bytes(constants.MacLenBytes, "little")) + out_.write(mac) + out_.write(constants.Padding.to_bytes(constants.MacFieldBytes - len(mac), "little")) + out_.write(ciphertext) + + +def encrypt_file(source_path: str, dest_path: str, data_key: bytes): + assert source_path != dest_path, "source_path = dest_path" + in_ = open(source_path, "rb") + out_ = open(dest_path, "wb") + + # header + file_len = os.path.getsize(source_path) + block_header_len = ( + constants.IvLenBytes + + constants.IvFieldBytes + + constants.MacLenBytes + + constants.MacFieldBytes + ) + block_data_len = constants.BlockBytes - block_header_len + package_cnt = (int)(file_len / block_data_len) + (file_len % block_data_len != 0) + + # write file header + out_.write(constants.Version.to_bytes(constants.VersionBytes, "little")) + out_.write(constants.Schema.to_bytes(constants.SchemaBytes, "little")) + out_.write(package_cnt.to_bytes(constants.PacketCntBytes, "little")) + out_.write(constants.BlockBytes.to_bytes(constants.BlockLenBytes, "little")) + + # write data block + for index in range(package_cnt - 1): + buf = in_.read(block_data_len) + encrypt_block(buf, data_key, out_) + + buf = in_.read(file_len - (package_cnt - 1) * block_data_len) + encrypt_block(buf, data_key, out_) + + out_.close() + in_.close() + + +def encrypt_file_inplace(file_path: str, data_key: bytes): + source_path = file_path + dest_path = file_path + "encrypted.tmp" + encrypt_file(source_path, dest_path, data_key) + os.remove(source_path) + os.rename(dest_path, source_path) + + +def decrypt_file_inplace(file_path: str, data_key: bytes): + source_path = file_path + dest_path = file_path + "decrypted.tmp" + decrypt_file(source_path, dest_path, data_key) + os.remove(source_path) + os.rename(dest_path, source_path) + + +def generate_rsa_keypair() -> (bytes, List[bytes]): + """Generate temp RSA key-pair""" + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=3072, + ) + public_key = private_key.public_key() + + builder = x509.CertificateBuilder() + builder = builder.subject_name( + x509.Name( + [ + x509.NameAttribute(NameOID.COMMON_NAME, "capsule-manager"), + ] + ) + ) + builder = builder.issuer_name( + x509.Name( + [ + x509.NameAttribute(NameOID.COMMON_NAME, "capsule-manager"), + ] + ) + ) + one_day = datetime.timedelta(1, 0, 0) + builder = builder.not_valid_before(datetime.datetime.today() - one_day) + builder = builder.not_valid_after(datetime.datetime.today() + (one_day * 30)) + builder = builder.serial_number(x509.random_serial_number()) + builder = builder.public_key(public_key) + certificate = builder.sign( + private_key=private_key, + algorithm=hashes.SHA256(), + ) + return ( + private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ), + [certificate.public_bytes(encoding=serialization.Encoding.PEM)], + ) + + +def convert_pem_to_der(pem: bytes) -> bytes: + cert = x509.load_pem_x509_certificate(pem) + return cert.public_bytes(encoding=serialization.Encoding.DER) diff --git a/capsule-manager-sdk/python/sdc/util/file.py b/capsule-manager-sdk/python/sdc/util/file.py new file mode 100644 index 0000000..beecbd0 --- /dev/null +++ b/capsule-manager-sdk/python/sdc/util/file.py @@ -0,0 +1,45 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 typing import Any + +from ruamel.yaml import YAML + +# init yaml +yaml = YAML() +yaml.preserve_quotes = True +yaml.indent(mapping=2, sequence=4, offset=2) +yaml.width = 40960 + + +def read_file(file_path: str, mode: str): + with open(file_path, mode) as f: + res = f.read() + return res + + +def write_file(file_path: str, mode: str, content: Any): + with open(file_path, mode) as f: + f.write(content) + + +def read_yaml_file(file_path: str): + with open(file_path) as f: + res = yaml.load(f) + return res + + +def write_yaml_file(content: dict, file_path: str): + with open(file_path, "w") as f: + yaml.dump(content, f) diff --git a/capsule-manager-sdk/python/sdc/util/tool.py b/capsule-manager-sdk/python/sdc/util/tool.py new file mode 100644 index 0000000..1f36d83 --- /dev/null +++ b/capsule-manager-sdk/python/sdc/util/tool.py @@ -0,0 +1,78 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 base64 + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from sdc.util import crypto + + +def assert_list_len_equal( + l1: list = None, l2: list = None, msg: str = "the len of list not equal" +): + if l1 is not None and l2 is not None: + assert len(l1) == len(l2), msg + + +def to_hex(data: bytes): + return "".join("{:02x}".format(x) for x in data) + + +def encode_base64(input_bytes: bytes, urlsafe: bool = True) -> str: + """Encode bytes as an unpadded base64 string.""" + + if urlsafe: + encode = base64.urlsafe_b64encode + else: + encode = base64.b64encode + + output_bytes = encode(input_bytes) + output_string = output_bytes.decode("ascii") + return output_string.rstrip("=") + + +def decode_base64(input_string: str) -> bytes: + """Decode an unpadded standard or urlsafe base64 string to bytes.""" + + input_bytes = input_string.encode("ascii") + input_len = len(input_bytes) + padding = b"=" * (3 - ((input_len + 3) % 4)) + + # Passing altchars here allows decoding both standard and urlsafe base64 + output_bytes = base64.b64decode(input_bytes + padding, altchars=b"-_") + return output_bytes + + +def generate_party_id(pk: rsa.RSAPublicKey) -> str: + party_id = base64.b32encode( + crypto.sha256( + pk.public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + ) + ) + return party_id.decode("ascii").rstrip("=") + + +def generate_party_id_from_cert(cert: bytes, format: str = "PEM") -> str: + if format == "PEM": + public_key = x509.load_pem_x509_certificate(cert).public_key() + elif format == "DER": + public_key = x509.load_der_x509_certificate(cert).public_key() + else: + raise RuntimeError(f"format {format} is not supported") + return generate_party_id(public_key) diff --git a/capsule-manager-sdk/python/setup.py b/capsule-manager-sdk/python/setup.py new file mode 100644 index 0000000..d401e24 --- /dev/null +++ b/capsule-manager-sdk/python/setup.py @@ -0,0 +1,62 @@ +# +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 datetime import date +from pathlib import Path + +import setuptools + +__version__ = "0.2.0.dev$$DATE$$" + + +def get_version(): + date_str = date.today().strftime("%Y%m%d") + return __version__.replace("$$DATE$$", date_str) + + +def read(fname): + with open(Path(__file__).resolve().parent / Path(fname)) as f: + return f.read() + + +if __name__ == "__main__": + setuptools.setup( + name="capsule-manager-sdk", + version=get_version(), + author="secretflow", + author_email="secretflow-contact@service.alipay.com", + description="Secure Data Capsule SDK for python", + long_description_content_type="text/markdown", + long_description="Secure Data Capsule SDK for python", + license="Apache 2.0", + url="https://github.com/secretflow/capsule-manager-sdk.git", + packages=setuptools.find_namespace_packages(exclude=("tests", "tests.*")), + install_requires=read("requirements.txt"), + entry_points=""" + [console_scripts] + cms=cli.cms:cms + cms_util=cli.cms_util:cms_util + cms_config=cli.cms_config:cms_config + """, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX :: Linux", + ], + options={ + "bdist_wheel": {"plat_name": "manylinux2014_x86_64"}, + }, + include_package_data=True, + ) diff --git a/capsule-manager-sdk/python/tests/__init__.py b/capsule-manager-sdk/python/tests/__init__.py new file mode 100644 index 0000000..7832e73 --- /dev/null +++ b/capsule-manager-sdk/python/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. diff --git a/capsule-manager-sdk/python/tests/crypto/test_asymm.py b/capsule-manager-sdk/python/tests/crypto/test_asymm.py new file mode 100644 index 0000000..e079833 --- /dev/null +++ b/capsule-manager-sdk/python/tests/crypto/test_asymm.py @@ -0,0 +1,49 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 unittest + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from sdc.crypto import asymm +from sdc.util import crypto + + +class TestAsymmCrypto(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestAsymmCrypto, self).__init__(*args, **kwargs) + + (private_key_pem, cert_pems) = crypto.generate_rsa_keypair() + self.private_key = private_key_pem + + cert = x509.load_pem_x509_certificate(cert_pems[0]) + self.pub_key_pem = cert.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + def test_rsa_encrypt(self): + data = b"hello world!" + secret = asymm.RsaEncryptor(self.pub_key_pem, "RSA-OAEP-256").encrypt(data) + result = asymm.RsaDecryptor(self.private_key, "RSA-OAEP-256").decrypt(secret) + self.assertEqual(data, result) + + def test_rsa_sign(self): + data = b"A message I want to sign" + sign = asymm.RsaSigner(self.private_key, "RS256").update(data).sign() + asymm.RsaVerifier(self.pub_key_pem, "RS256").update(data).verify(sign) + + +if __name__ == "__main__": + unittest.main() diff --git a/capsule-manager-sdk/python/tests/crypto/test_symm.py b/capsule-manager-sdk/python/tests/crypto/test_symm.py new file mode 100644 index 0000000..1f5e59f --- /dev/null +++ b/capsule-manager-sdk/python/tests/crypto/test_symm.py @@ -0,0 +1,39 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 unittest + +from sdc.crypto import symm +from sdc.util import crypto + + +class TestSymmCrypto(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestSymmCrypto, self).__init__(*args, **kwargs) + self.secret_key = crypto.gen_key(16) + + def test_aes_gcm_encrypt(self): + data = b"hello world!" + iv = crypto.gen_key(12) + (ciphertext, tag) = symm.AesGcmEncryptor(self.secret_key, "A128GCM").encrypt( + data, iv, b"" + ) + result = symm.AesGcmDecryptor(self.secret_key, "A128GCM").decrypt( + ciphertext, iv, b"", tag + ) + self.assertEqual(data, result) + + +if __name__ == "__main__": + unittest.main() diff --git a/capsule-manager-sdk/python/tests/test_capsule_manager.py b/capsule-manager-sdk/python/tests/test_capsule_manager.py new file mode 100644 index 0000000..f1f2ddd --- /dev/null +++ b/capsule-manager-sdk/python/tests/test_capsule_manager.py @@ -0,0 +1,126 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 base64 +import socket +import unittest + +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from sdc.capsule_manager_frame import CapsuleManagerFrame +from sdc.util import crypto +from tests.util.mock_capsule_manager import start_server + + +def pick_unused_port(): + sock = socket.socket() + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +class TestCapsuleManager(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestCapsuleManager, self).__init__(*args, **kwargs) + + self.port = pick_unused_port() + self.server = start_server(self.port) + + (self.pri_key_pem, self.cert_pems) = crypto.generate_rsa_keypair() + + def test_get_pk(self): + auth_frame = CapsuleManagerFrame( + f"127.0.0.1:{self.port}", + "sim", + "1083D6017E951017EB29611024D63D4DF73445DD880D1151E776541FEBE4A776", + None, + ) + public_key_pem = auth_frame.get_public_key() + self.assertGreater(len(public_key_pem), 0) + + def test_data_keys(self): + auth_frame = CapsuleManagerFrame( + f"127.0.0.1:{self.port}", + "sim", + "1083D6017E951017EB29611024D63D4DF73445DD880D1151E776541FEBE4A776", + None, + ) + auth_frame.register_cert("alice", self.cert_pems, "RSA", self.pri_key_pem) + data_key_b64 = base64.b64encode(AESGCM.generate_key(bit_length=128)) + data_keys = [{"resource_uri": "dataA", "data_key_b64": data_key_b64}] + auth_frame.create_data_keys("alice", data_keys, None, self.pri_key_pem) + rules = [ + { + "rule_id": "rule1", + "grantee_party_ids": ["bob"], + "columns": ["id"], + "global_constraints": None, + "op_constraints": [{"op_name": "*", "constraints": None}], + } + ] + auth_frame.create_data_policy( + "alice", + "default", + "dataA", + rules, + None, + self.pri_key_pem, + ) + + result = auth_frame.get_data_policys("alice", "default", None, self.pri_key_pem) + + self.assertEqual(len(result), 1) + self.assertEqual(len(result[0].rules), 1) + + rule = { + "rule_id": "rule2", + "grantee_party_ids": ["carol"], + "columns": ["name"], + "global_constraints": None, + "op_constraints": [ + { + "op_name": "OP_PSI", + "constraints": ["r.env.sgx.mr_enclave=mr_enclave"], + } + ], + } + auth_frame.add_data_rule( + "alice", + "default", + "dataA", + rule, + None, + self.pri_key_pem, + ) + result = auth_frame.get_data_policys("alice", "default", None, self.pri_key_pem) + + self.assertEqual(len(result), 1) + self.assertEqual(len(result[0].rules), 2) + + auth_frame.delete_data_rule( + "alice", "default", "dataA", "rule1", None, self.pri_key_pem + ) + result = auth_frame.get_data_policys("alice", "default", None, self.pri_key_pem) + + self.assertEqual(len(result), 1) + self.assertEqual(len(result[0].rules), 1) + + auth_frame.delete_data_policy( + "alice", "default", "dataA", None, self.pri_key_pem + ) + result = auth_frame.get_data_policys("alice", "default", None, self.pri_key_pem) + + self.assertEqual(len(result), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/capsule-manager-sdk/python/tests/util/__init__.py b/capsule-manager-sdk/python/tests/util/__init__.py new file mode 100644 index 0000000..7832e73 --- /dev/null +++ b/capsule-manager-sdk/python/tests/util/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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. diff --git a/capsule-manager-sdk/python/tests/util/mock_capsule_manager.py b/capsule-manager-sdk/python/tests/util/mock_capsule_manager.py new file mode 100644 index 0000000..bd4fd89 --- /dev/null +++ b/capsule-manager-sdk/python/tests/util/mock_capsule_manager.py @@ -0,0 +1,274 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 concurrent import futures +from typing import Union + +import grpc +from cryptography import x509 +from cryptography.hazmat.primitives.asymmetric import rsa +from google.protobuf import json_format, message +from sdc.crypto import asymm, symm +from sdc.util import crypto, tool +from secretflowapis.v2 import status_pb2 +from secretflowapis.v2.sdc import jwt_pb2 +from secretflowapis.v2.sdc.capsule_manager import ( + capsule_manager_pb2, + capsule_manager_pb2_grpc, +) + + +class CapsuleManagerServer(capsule_manager_pb2_grpc.CapsuleManagerServicer): + def __init__(self): + (self._pri_key, self._certs) = crypto.generate_rsa_keypair() + self._data_policys = {} + self._data_keys = {} + self._party_cert = {} + + def encrypt_response( + self, + msg: message.Message, + public_key_pem: Union[bytes, rsa.RSAPublicKey], + ) -> capsule_manager_pb2.EncryptedResponse: + data_key = crypto.gen_key(16) + iv = crypto.gen_key(32) + add = b"" + + jwe = jwt_pb2.Jwe() + jwe_header = jwe.JoseHeader() + jwe_header.alg = "RSA-OAEP-256" + jwe_header.enc = "A128GCM" + + (ciphertext, tag) = symm.AesGcmEncryptor(data_key, jwe_header.enc).encrypt( + json_format.MessageToJson(msg).encode("utf-8"), iv, add + ) + encrypted_data_key = asymm.RsaEncryptor(public_key_pem, jwe_header.alg).encrypt( + data_key + ) + jwe.protected_header = tool.encode_base64( + json_format.MessageToJson(jwe_header).encode("utf-8") + ) + jwe.encrypted_key = tool.encode_base64(encrypted_data_key) + jwe.iv = tool.encode_base64(iv) + jwe.aad = "" + jwe.ciphertext = tool.encode_base64(ciphertext) + jwe.tag = tool.encode_base64(tag) + + return capsule_manager_pb2.EncryptedResponse( + status=status_pb2.Status(code=0), message=jwe + ) + + def parse_encrypted_request( + self, + request: capsule_manager_pb2.EncryptedRequest, + private_key: Union[bytes, str, rsa.RSAPrivateKey], + msg: message.Message, + ): + jwe = request.message + jwe_header = jwe.JoseHeader() + json_format.Parse(tool.decode_base64(jwe.protected_header), jwe_header) + + iv = tool.decode_base64(jwe.iv) + ciphertext = tool.decode_base64(jwe.ciphertext) + tag = tool.decode_base64(jwe.tag) + add = tool.decode_base64(jwe.aad) + + data_key = asymm.RsaDecryptor(private_key, jwe_header.alg).decrypt( + tool.decode_base64(jwe.encrypted_key) + ) + plain_text = symm.AesGcmDecryptor(data_key, jwe_header.enc).decrypt( + ciphertext, iv, add, tag + ) + + if not request.has_signature: + json_format.Parse(plain_text, msg) + else: + jws = jwt_pb2.Jws() + json_format.Parse(plain_text, jws) + json_format.Parse(tool.decode_base64(jws.payload), msg) + + def GetRaCert( + self, request: capsule_manager_pb2.GetRaCertRequest, context + ) -> capsule_manager_pb2.GetRaCertResponse: + return capsule_manager_pb2.GetRaCertResponse( + status=status_pb2.Status(code=0), + attestation_report=None, + cert=self._certs[0], + ) + + def GetDataKeys( + self, request: capsule_manager_pb2.EncryptedRequest, context + ) -> capsule_manager_pb2.EncryptedResponse: + request_content = capsule_manager_pb2.GetDataKeysRequest() + self.parse_encrypted_request(request, self._pri_key, request_content) + resource_uris = [ + resource.resource_uri + for resource in request_content.resource_request.resources + ] + response_content = capsule_manager_pb2.GetDataKeysResponse() + for resource_uri in resource_uris: + if resource_uri in self._data_keys: + response_content.data_keys.add( + resource_uri=resource_uri, + data_key_b64=self._data_keys[resource_uri][0], + ) + response_content.cert = self._certs[0] + return self.encrypt_response( + response_content, + x509.load_pem_x509_certificate( + request_content.cert.encode("utf-8") + ).public_key(), + ) + + def CreateDataPolicy( + self, request: capsule_manager_pb2.EncryptedRequest, context + ) -> capsule_manager_pb2.EncryptedResponse: + request_content = capsule_manager_pb2.CreateDataPolicyRequest() + self.parse_encrypted_request(request, self._pri_key, request_content) + + key = "{}/{}".format(request_content.scope, request_content.policy.data_uuid) + self._data_policys[key] = ( + request_content.policy, + request_content.owner_party_id, + request_content.scope, + ) + return capsule_manager_pb2.EncryptedResponse( + status=status_pb2.Status(code=0), message=None + ) + + def ListDataPolicy( + self, request: capsule_manager_pb2.EncryptedRequest, context + ) -> capsule_manager_pb2.EncryptedResponse: + request_content = capsule_manager_pb2.ListDataPolicyRequest() + self.parse_encrypted_request(request, self._pri_key, request_content) + response_content = capsule_manager_pb2.ListDataPolicyResponse() + for value in self._data_policys.values(): + (policy, party_id, scope) = (value[0], value[1], value[2]) + if ( + request_content.owner_party_id == party_id + and request_content.scope == scope + ): + response_content.policies.append(policy) + return self.encrypt_response( + response_content, + x509.load_pem_x509_certificate( + self._party_cert[request_content.owner_party_id].encode("utf-8") + ).public_key(), + ) + + def AddDataRule( + self, request: capsule_manager_pb2.EncryptedRequest, context + ) -> capsule_manager_pb2.EncryptedResponse: + request_content = capsule_manager_pb2.AddDataRuleRequest() + self.parse_encrypted_request(request, self._pri_key, request_content) + key = "{}/{}".format(request_content.scope, request_content.data_uuid) + if key in self._data_policys: + if ( + self._data_policys[key][0].data_uuid == request_content.data_uuid + and self._data_policys[key][1] == request_content.owner_party_id + ): + self._data_policys[key][0].rules.append(request_content.rule) + else: + raise RuntimeError("data uuid or party id is wrong") + else: + self._data_policys[key] = ( + capsule_manager_pb2.Policy( + data_uuid=request_content.data_uuid, rules=[request_content.rule] + ), + request_content.owner_party_id, + request_content.scope, + ) + return capsule_manager_pb2.EncryptedResponse( + status=status_pb2.Status(code=0), message=None + ) + + def DeleteDataPolicy( + self, request: capsule_manager_pb2.EncryptedRequest, context + ) -> capsule_manager_pb2.EncryptedResponse: + request_content = capsule_manager_pb2.DeleteDataPolicyRequest() + self.parse_encrypted_request(request, self._pri_key, request_content) + key = "{}/{}".format(request_content.scope, request_content.data_uuid) + if key in self._data_policys: + if ( + self._data_policys[key][0].data_uuid == request_content.data_uuid + and self._data_policys[key][1] == request_content.owner_party_id + ): + self._data_policys.pop(key, None) + else: + raise RuntimeError("data uuid or party id is wrong") + return capsule_manager_pb2.EncryptedResponse( + status=status_pb2.Status(code=0), message=None + ) + + def DeleteDataRule( + self, request: capsule_manager_pb2.EncryptedRequest, context + ) -> capsule_manager_pb2.EncryptedResponse: + request_content = capsule_manager_pb2.DeleteDataRuleRequest() + self.parse_encrypted_request(request, self._pri_key, request_content) + key = "{}/{}".format(request_content.scope, request_content.data_uuid) + if key in self._data_policys: + if ( + self._data_policys[key][0].data_uuid == request_content.data_uuid + and self._data_policys[key][1] == request_content.owner_party_id + ): + remove_index = -1 + for index in range(len(self._data_policys[key][0].rules)): + if ( + self._data_policys[key][0].rules[index].rule_id + == request_content.rule_id + ): + remove_index = index + break + if remove_index != -1: + del self._data_policys[key][0].rules[remove_index] + else: + raise RuntimeError("data uuid or party id is wrong") + return capsule_manager_pb2.EncryptedResponse( + status=status_pb2.Status(code=0), message=None + ) + + def CreateDataKeys( + self, request: capsule_manager_pb2.EncryptedRequest, context + ) -> capsule_manager_pb2.EncryptedResponse: + request_content = capsule_manager_pb2.CreateDataKeysRequest() + self.parse_encrypted_request(request, self._pri_key, request_content) + for data_key in request_content.data_keys: + self._data_keys[data_key.resource_uri] = ( + data_key.data_key_b64, + request_content.owner_party_id, + ) + + return capsule_manager_pb2.EncryptedResponse( + status=status_pb2.Status(code=0), message=None + ) + + def RegisterCert( + self, request: capsule_manager_pb2.EncryptedRequest, context + ) -> capsule_manager_pb2.EncryptedResponse: + request_content = capsule_manager_pb2.RegisterCertRequest() + self.parse_encrypted_request(request, self._pri_key, request_content) + self._party_cert[request_content.owner_party_id] = request_content.certs[0] + return capsule_manager_pb2.EncryptedResponse( + status=status_pb2.Status(code=0), message=None + ) + + +def start_server(port): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=5)) + capsule_manager_pb2_grpc.add_CapsuleManagerServicer_to_server( + CapsuleManagerServer(), server + ) + server.add_insecure_port(f"[::]:{port}") + server.start() + return server diff --git a/capsule-manager-sdk/python/tests/util/test_util.py b/capsule-manager-sdk/python/tests/util/test_util.py new file mode 100644 index 0000000..48b2f73 --- /dev/null +++ b/capsule-manager-sdk/python/tests/util/test_util.py @@ -0,0 +1,56 @@ +# Copyright 2023 Ant Group Co., Ltd. +# +# 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 unittest + +from sdc.util import crypto, file + +SourceFilename = "source.dat" +EncryptionFilename = "encryption.dat" +DecryptionFilename = "decryption.dat" + + +class TestUtil(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestUtil, self).__init__(*args, **kwargs) + self.secret_key = crypto.gen_key(16) + + def test_file_encrypt(self): + raw_data = b"hello world" + file.write_file(SourceFilename, "wb", raw_data) + crypto.encrypt_file(SourceFilename, EncryptionFilename, self.secret_key) + + crypto.decrypt_file(EncryptionFilename, DecryptionFilename, self.secret_key) + data = file.read_file(DecryptionFilename, "rb") + self.assertEqual(data, raw_data) + os.remove(SourceFilename) + os.remove(EncryptionFilename) + os.remove(DecryptionFilename) + + def test_file_encrypt_inplace(self): + raw_data = b"hello world" + file.write_file(SourceFilename, "wb", raw_data) + crypto.encrypt_file_inplace(SourceFilename, self.secret_key) + data = file.read_file(SourceFilename, "rb") + self.assertNotEqual(data, raw_data) + + crypto.decrypt_file_inplace(SourceFilename, self.secret_key) + data = file.read_file(SourceFilename, "rb") + self.assertEqual(data, raw_data) + os.remove(SourceFilename) + + +if __name__ == "__main__": + unittest.main() diff --git a/capsule-manager/Cargo.toml b/capsule-manager/Cargo.toml index 6d78680..c6655d0 100644 --- a/capsule-manager/Cargo.toml +++ b/capsule-manager/Cargo.toml @@ -39,14 +39,13 @@ hex = "0.4.3" base32 = "0.4.0" lazy_static = "1.4.0" sdc_apis = "0.2.1-dev20240222" -trustedflow-attestation-rs = { version = "0.1.2-dev240313"} +trustflow-attestation-rs = { version = "0.3.1-dev20240726"} sea-orm = { version = "^0.12.0", features = [ "sqlx-mysql", "runtime-tokio-native-tls", "macros" ] } sqlx = { version = "0.7", features = [ "runtime-tokio" ] } eyre = "0.6.11" sqlx-core = "0.7.4" axum = "0.7.5" async-trait = "0.1.80" - [dependencies.uuid] version = "1.8.0" features = [ diff --git a/capsule-manager/src/server.rs b/capsule-manager/src/server.rs index c1b52ca..1b188f2 100644 --- a/capsule-manager/src/server.rs +++ b/capsule-manager/src/server.rs @@ -245,7 +245,7 @@ fn launch_check() -> AuthResult<()> { }), }; - trustedflow_attestation_rs::generate_attestation_report( + trustflow_attestation_rs::generate_attestation_report( serde_json::to_string(&report_params) .map_err(|e| { errno!( diff --git a/capsule-manager/src/server/key_management_impl.rs b/capsule-manager/src/server/key_management_impl.rs index 19bb25c..54f5391 100644 --- a/capsule-manager/src/server/key_management_impl.rs +++ b/capsule-manager/src/server/key_management_impl.rs @@ -45,7 +45,7 @@ fn ra_verify( // fill policy let mut verified_attributes = UnifiedAttestationAttributes::default(); - let attributes = trustedflow_attestation_rs::parse_attributes_from_report(report_json_str) + let attributes = trustflow_attestation_rs::parse_attributes_from_report(report_json_str) .map_err(|e| { errno!( ErrorCode::InvalidArgument, @@ -76,7 +76,7 @@ fn ra_verify( e ) })?; - trustedflow_attestation_rs::attestation_report_verify( + trustflow_attestation_rs::attestation_report_verify( report_json_str, policy_json_str.as_str(), ) @@ -88,7 +88,7 @@ fn ra_verify( ) })?; Ok( - trustedflow_attestation_rs::parse_attributes_from_report(report_json_str).map_err(|e| { + trustflow_attestation_rs::parse_attributes_from_report(report_json_str).map_err(|e| { errno!( ErrorCode::InvalidArgument, "parse report attributes failed: {:?}.", diff --git a/capsule-manager/src/server/ra_impl.rs b/capsule-manager/src/server/ra_impl.rs index d75d91a..f46b8fb 100644 --- a/capsule-manager/src/server/ra_impl.rs +++ b/capsule-manager/src/server/ra_impl.rs @@ -52,7 +52,7 @@ impl CapsuleManagerImpl { }), }; - let report_json = trustedflow_attestation_rs::generate_attestation_report( + let report_json = trustflow_attestation_rs::generate_attestation_report( serde_json::to_string(&report_params) .map_err(|e| { errno!( diff --git a/capsule-manager/src/storage/sql_storage/cm.sql b/capsule-manager/src/storage/sql_storage/cm.sql index 4f9da25..ce590cd 100644 --- a/capsule-manager/src/storage/sql_storage/cm.sql +++ b/capsule-manager/src/storage/sql_storage/cm.sql @@ -33,3 +33,5 @@ CREATE TABLE `rules` ( PRIMARY KEY (`rule_id`), FOREIGN KEY (`resource_uri`) REFERENCES data_meta(`resource_uri`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + diff --git a/deployment/Dockerfile b/deployment/Dockerfile index 5dd5f15..36def3d 100644 --- a/deployment/Dockerfile +++ b/deployment/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM secretflow/trustedflow-dev-ubuntu22.04:latest as builder +FROM secretflow/trustflow-dev-ubuntu22.04:latest as builder ARG PLATFORM @@ -27,11 +27,12 @@ COPY script ./script RUN ./script/build.sh -p $PLATFORM -FROM secretflow/trustedflow-release-ubuntu22.04:latest +FROM secretflow/trustflow-release-ubuntu22.04:latest COPY --from=builder /lib/libverification.so /home/admin/lib/libverification.so COPY --from=builder /lib/libgeneration.so /home/admin/lib/libgeneration.so -COPY --from=builder /home/admin/dev/target/release/grpc-as /home/admin/capsule_manager +COPY --from=builder /home/admin/dev/target/release/http-as /home/admin/capsule_manager_http +COPY --from=builder /home/admin/dev/target/release/grpc-as /home/admin/capsule_manager_grpc COPY --from=builder /home/admin/dev/script/conf/config.yaml /home/admin/config.yaml COPY deployment/entrypoint.sh /home/admin/entrypoint.sh diff --git a/deployment/entrypoint.sh b/deployment/entrypoint.sh index 39c487e..daea6ce 100755 --- a/deployment/entrypoint.sh +++ b/deployment/entrypoint.sh @@ -18,4 +18,4 @@ LD_LIBRARY_PATH=/home/admin/lib && \ /home/admin/capsule_manager \ --tls_config.enable_tls false --log_config.log_dir log \ --log_config.enable_console_logger true \ - --config_path config.yaml $@ + --config_path config.yaml $@ \ No newline at end of file diff --git a/deployment/occlum.Dockerfile b/deployment/occlum.Dockerfile index 4441c71..0ab5030 100644 --- a/deployment/occlum.Dockerfile +++ b/deployment/occlum.Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM secretflow/trustedflow-dev-occlum-ubuntu22.04:latest as builder +FROM secretflow/trustflow-dev-occlum-ubuntu22.04:latest as builder ARG PLATFORM @@ -27,7 +27,7 @@ COPY script ./script RUN ./script/build.sh -p $PLATFORM -FROM secretflow/trustedflow-release-occlum-ubuntu22.04:latest +FROM secretflow/trustflow-release-occlum-ubuntu22.04:latest # for occlum build, we need to install following pkgs RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y \ diff --git a/deployment/publish.sh b/deployment/publish.sh index 27dea06..8e2de44 100755 --- a/deployment/publish.sh +++ b/deployment/publish.sh @@ -38,7 +38,7 @@ show_help() { SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" cd $SCRIPT_DIR/.. -DOCKER_REGS="secretflow" +DOCKER_REG="secretflow" # 解析短选项的getopts循环 while getopts "p:v:e:luh" opt; do @@ -104,6 +104,4 @@ for repo in "${DOCKER_REGS[@]}"; do docker push $repo/$LATEST_TAG fi fi -done - - +done \ No newline at end of file diff --git a/env.sh b/env.sh index 4158f7b..40b3a12 100755 --- a/env.sh +++ b/env.sh @@ -15,8 +15,8 @@ # #!/bin/bash DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -OCCLUM_DEV_IMAGE=secretflow/trustedflow-dev-occlum-ubuntu22.04:latest -DEV_IMAGE=secretflow/trustedflow-dev-ubuntu22.04:latest +OCCLUM_DEV_IMAGE=secretflow/trustflow-dev-occlum-ubuntu22.04:latest +DEV_IMAGE=secretflow/trustflow-dev-ubuntu22.04:latest DOCKER=docker PROJECT=capsule-manager-dev-ubuntu diff --git a/script/build.sh b/script/build.sh index 8d4368f..d166551 100755 --- a/script/build.sh +++ b/script/build.sh @@ -55,14 +55,14 @@ bash install_attestation_lib.sh -p $PLATFORM case "$PLATFORM" in sgx) - /root/.cargo/bin/cargo build -p grpc-as --release --features production + /root/.cargo/bin/cargo build -p grpc-as -p http-as --release --features production bash build_occlum.sh ;; sim) - /root/.cargo/bin/cargo build -p grpc-as --release + /root/.cargo/bin/cargo build -p grpc-as -p http-as --release ;; tdx|csv) - /root/.cargo/bin/cargo build -p grpc-as --release --features production + /root/.cargo/bin/cargo build -p grpc-as -p http-as --release --features production ;; *) echo -e "PLATFORM does not match any of options(sim/sgx/tdx/csv)" diff --git a/script/build_occlum.sh b/script/build_occlum.sh index e560914..ddb6ac6 100755 --- a/script/build_occlum.sh +++ b/script/build_occlum.sh @@ -39,7 +39,8 @@ echo -e "${GREEN} ===== Initailize occlum workspace end ===== ${NC}" pushd $occlum_instance_dir mkdir -p image/bin/ -cp $workspace_dir/target/release/grpc-as image/bin/capsule_manager +cp $workspace_dir/target/release/http-as image/bin/capsule_manager_http +cp $workspace_dir/target/release/grpc-as image/bin/capsule_manager_grpc # Copy glibc so to image. mkdir -p image/opt/occlum/glibc/lib/ @@ -56,7 +57,7 @@ cp -a /opt/occlum/glibc/lib/libnss_dns.so* \ cp -an /usr/lib/x86_64-linux-gnu/lib*so* . popd -# trustedflow attestation lib +# trustflow attestation lib mkdir -p image/usr/local/lib cp /lib/libgeneration.so image/usr/local/lib cp /lib/libverification.so image/usr/local/lib diff --git a/script/conf/config.yaml b/script/conf/config.yaml index 1d9a09c..2b6ad8f 100644 --- a/script/conf/config.yaml +++ b/script/conf/config.yaml @@ -31,3 +31,4 @@ storage_config: scheme: "RSA" # Asymmetric key generation method, SM2/RSA enable_inject_cm_key: false + diff --git a/script/install_attestation_lib.sh b/script/install_attestation_lib.sh old mode 100755 new mode 100644 index 1d5c89d..d76fda6 --- a/script/install_attestation_lib.sh +++ b/script/install_attestation_lib.sh @@ -52,40 +52,40 @@ NC="\033[0m" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" cd $SCRIPT_DIR -rm -rf trustedflow -git clone https://github.com/secretflow/trustedflow.git -cd trustedflow +rm -rf trustflow +git clone https://github.com/asterinas/trustflow.git +cd trustflow -echo "build trustedflow attestation" +echo "build trustflow attestation" case "$PLATFORM" in sim) - bazel build -c opt //trustedflow/... + bazel build -c opt //trustflow/... ;; sgx) - bazel build -c opt --define tee_type=sgx2 //trustedflow/... + bazel build -c opt --define tee_type=sgx2 //trustflow/... ;; tdx) - bazel build -c opt --define tee_type=tdx //trustedflow/... + bazel build -c opt --define tee_type=tdx //trustflow/... ;; csv) - bazel build -c opt --define tee_type=csv //trustedflow/... + bazel build -c opt --define tee_type=csv //trustflow/... ;; *) echo -e "PLATFORM does not match any of options(sim/sgx/tdx/csv)" exit 1 ;; esac -echo "build trustedflow attestation successfully" +echo "build trustflow attestation successfully" if [ -z "$SAVE_PATH" ]; then SAVE_PATH="/lib" fi -cp bazel-bin/trustedflow/attestation/generation/wrapper/libgeneration.so /lib +cp bazel-bin/trustflow/attestation/generation/wrapper/libgeneration.so /lib echo "copy libgeneration.so to $SAVE_PATH successfully" -cp bazel-bin/trustedflow/attestation/verification/wrapper/libverification.so /lib +cp bazel-bin/trustflow/attestation/verification/wrapper/libverification.so /lib echo "copy libverication.so to $SAVE_PATH successfully" -cd .. && rm -rf trustedflow +cd .. && rm -rf trustflow