From d71874f30694278fcabe1e1f5eedcf8e07cea22c Mon Sep 17 00:00:00 2001 From: Daniel Duan Date: Mon, 25 Sep 2023 16:34:40 +0800 Subject: [PATCH] CDH: add en/decrypt support for eHSM-KMS Signed-off-by: Daniel Duan --- confidential-data-hub/kms/Cargo.toml | 4 +- confidential-data-hub/kms/src/error.rs | 4 + .../kms/src/plugins/ehsm/README.md | 116 +++++++++ .../kms/src/plugins/ehsm/annotations.rs | 13 + .../kms/src/plugins/ehsm/client.rs | 226 ++++++++++++++++++ .../kms/src/plugins/ehsm/credential.rs | 16 ++ .../credential.4eb1____.json | 4 + .../kms/src/plugins/ehsm/mod.rs | 15 ++ confidential-data-hub/kms/src/plugins/mod.rs | 11 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 confidential-data-hub/kms/src/plugins/ehsm/README.md create mode 100644 confidential-data-hub/kms/src/plugins/ehsm/annotations.rs create mode 100644 confidential-data-hub/kms/src/plugins/ehsm/client.rs create mode 100644 confidential-data-hub/kms/src/plugins/ehsm/credential.rs create mode 100644 confidential-data-hub/kms/src/plugins/ehsm/example_credential/credential.4eb1____.json create mode 100644 confidential-data-hub/kms/src/plugins/ehsm/mod.rs diff --git a/confidential-data-hub/kms/Cargo.toml b/confidential-data-hub/kms/Cargo.toml index 2492ffb97..7c0c58e98 100644 --- a/confidential-data-hub/kms/Cargo.toml +++ b/confidential-data-hub/kms/Cargo.toml @@ -13,6 +13,7 @@ bincode = { workspace = true, optional = true } chrono = { workspace = true, optional = true } const_format.workspace = true crypto = { path = "../../attestation-agent/deps/crypto", optional = true } +ehsm_client = {git = "https://github.com/intel/ehsm", rev = "f84688688e724dfd080c1dc491db3e58415cc5b7", optional = true } hex = { workspace = true, optional = true } kbs_protocol = { path = "../../attestation-agent/kbs_protocol", default-features = false, features = ["passport", "aa_token", "openssl"], optional = true } lazy_static.workspace = true @@ -41,8 +42,9 @@ anyhow.workspace = true tonic-build.workspace = true [features] -default = ["aliyun", "kbs"] +default = ["aliyun", "kbs", "ehsm"] aliyun = ["chrono", "hex", "openssl", "prost", "reqwest", "sha2", "tonic"] kbs = ["kbs_protocol"] +ehsm = ["ehsm_client"] sev = ["bincode", "crypto", "dep:sev", "prost", "tonic", "uuid", "zeroize"] diff --git a/confidential-data-hub/kms/src/error.rs b/confidential-data-hub/kms/src/error.rs index 6cf657844..980d352bb 100644 --- a/confidential-data-hub/kms/src/error.rs +++ b/confidential-data-hub/kms/src/error.rs @@ -16,6 +16,10 @@ pub enum Error { #[error("Kbs client error: {0}")] KbsClientError(String), + #[cfg(feature = "ehsm")] + #[error("eHSM-KMS client error: {0}")] + EhsmKmsError(String), + #[error("Unsupported provider: {0}")] UnsupportedProvider(String), } diff --git a/confidential-data-hub/kms/src/plugins/ehsm/README.md b/confidential-data-hub/kms/src/plugins/ehsm/README.md new file mode 100644 index 000000000..128c63076 --- /dev/null +++ b/confidential-data-hub/kms/src/plugins/ehsm/README.md @@ -0,0 +1,116 @@ +# eHSM-KMS + +eHSM-KMS is a SGX-based Key Managment Service (KMS) that provides the near-equivalent hardware protection level of cryptographic functionalities including key generation, management inside the SGX enclave. More information about eHSM-KMS can be found [here](https://github.com/intel/ehsm). + +In CDH, we provide the eHSM-KMS client to interact with the eHSM-KMS Server. + +## eHSM-KMS Service + +For eHSM-KMS client to run, you need to set up an eHSM-KMS service in advance. The following method is only a quick start, and you can find more deployment methods (e.g. with Kubernetes) at webpage of eHSM-KMS. + +> Prerequisite: a sgx capable machine + +* Install requirement tools + ``` shell + sudo apt update + + sudo apt install vim autoconf automake build-essential cmake curl debhelper git libcurl4-openssl-dev libprotobuf-dev libssl-dev libtool lsb-release ocaml ocamlbuild protobuf-compiler wget libcurl4 libssl1.1 make g++ fakeroot libelf-dev libncurses-dev flex bison libfdt-dev libncursesw5-dev pkg-config libgtk-3-dev libspice-server-dev libssh-dev python3 python3-pip reprepro unzip libjsoncpp-dev uuid-dev liblog4cplus-1.1-9 liblog4cplus-dev dnsutils + ``` + +* Install SGX SDK + ```shell + wget https://download.01.org/intel-sgx/sgx-linux/2.18/as.ld.objdump.r4.tar.gz + tar -zxf as.ld.objdump.r4.tar.gz + sudo cp external/toolset/{current_distr}/* /usr/local/bin + + wget https://download.01.org/intel-sgx/sgx-dcap/1.15/linux/distro/ubuntu20.04-server/sgx_linux_x64_sdk_2.18.100.3.bin + + #choose to install the sdk into the /opt/intel + chmod a+x ./sgx_linux_x64_sdk_2.18.100.3.bin && sudo ./sgx_linux_x64_sdk_2.18.100.3.bin + + source /opt/intel/sgxsdk/environment + ``` + +* Install DCAP required packages + ```shell + cd /opt/intel + + wget https://download.01.org/intel-sgx/sgx-dcap/1.15/linux/distro/ubuntu20.04-server/sgx_debian_local_repo.tgz + + tar xzf sgx_debian_local_repo.tgz + + echo 'deb [trusted=yes arch=amd64] file:///opt/intel/sgx_debian_local_repo focal main' | sudo tee /etc/apt/sources.list.d/intel-sgx.list + + wget -qO - https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key | sudo apt-key add - + + sudo apt-get update + + sudo apt-get install -y libsgx-enclave-common-dev libsgx-ae-qe3 libsgx-ae-qve libsgx-urts libsgx-dcap-ql libsgx-dcap-default-qpl libsgx-dcap-quote-verify-dev libsgx-dcap-ql-dev libsgx-dcap-default-qpl-dev libsgx-quote-ex-dev libsgx-uae-service libsgx-ra-network libsgx-ra-uefi + ``` + +* Change PCCS server IP + ``` shell + vim /etc/sgx_default_qcnl.conf + ``` + ``` vi + # PCCS server address + PCCS_URL=https://1.2.3.4:8081/sgx/certification/v3/ (your pccs IP) + + # To accept insecure HTTPS certificate, set this option to FALSE + USE_SECURE_CERT=FALSE + ``` + +* Either start eHSM-KMS on a single machine without remote attestation. + ``` + # run eHSM-KMS + ./run_with_single.sh + ``` + +* Or build and run eHSM-KMS with docker-compose: + ```shell + # Download the current stable release (remove the "-x $http_proxy" if you don't behind the proxy) + sudo curl -x $http_proxy -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose + docker-compose --version + # docker-compose version 1.29.2, build 5becea4c + + # Download the ehsm code from github + git clone --recursive https://github.com/intel/ehsm.git ehsm && cd ehsm + vim docker/.env + + # Modify the docker/.env configurations + HOST_IP=1.2.3.4 # MUST modify it to your host IP. + PCCS_URL=https://1.2.3.4:8081 # MUST modify it to your pccs server url. + DKEYSERVER_PORT=8888 # (Optional) the default port of dkeyserver, modify it if you want. + KMS_PORT=9000 # (Optional) the default KMS port, modify it if you want. + TAG_VERSION=main # (Optional) the default code base is using the main latest branch, modify it to specific tag if you want. + + # start to build and run the docker images (couchdb, dkeyserver, dkeycache, ehsm_kms_service) + cd docker && docker-compose up -d + ``` + +* Enrollment of the APPID and APIKey + ```shell + curl -v -k -G "https://:/ehsm?Action=Enroll" + + {"code":200,"message":"successful","result":{"apikey":"xbtXGHwBexb1pgnEz8JZWHLgaSVb1xSk","appid":"56c46c76-60e0-4722-a6ad-408cdd0c62c2"}} + ``` + +* Run the unittest cases + ``` shell + cd test + # run the unit testcases + python3 test_kms_with_cli.py --url https://: + ``` + +Congratulations! eHSM-KMS service should be ready by now. + +# eHSM-KMS Client + +eHSM-KMS client requires a credential file to run. The file name of the credential file is `credential.{your_app_id}.json`. The credential file need to be placed in `/run/confidential-containers/cdh/kms-credential/ehsm/`. And the structure of the credential file is shown in `ehsm/example_credential/` folder. + +To test eHSM-KMS client, run +```bash +cargo test --features ehsm +``` \ No newline at end of file diff --git a/confidential-data-hub/kms/src/plugins/ehsm/annotations.rs b/confidential-data-hub/kms/src/plugins/ehsm/annotations.rs new file mode 100644 index 000000000..41de08109 --- /dev/null +++ b/confidential-data-hub/kms/src/plugins/ehsm/annotations.rs @@ -0,0 +1,13 @@ +// Copyright (c) 2023 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +use serde::{Deserialize, Serialize}; + +/// Serialized [`crate::ProviderSettings`] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EhsmProviderSettings { + pub app_id: String, + pub endpoint: String, +} diff --git a/confidential-data-hub/kms/src/plugins/ehsm/client.rs b/confidential-data-hub/kms/src/plugins/ehsm/client.rs new file mode 100644 index 000000000..339b63bd7 --- /dev/null +++ b/confidential-data-hub/kms/src/plugins/ehsm/client.rs @@ -0,0 +1,226 @@ +// Copyright (c) 2023 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +use ehsm_client::{api::KMS, client::EHSMClient}; + +use async_trait::async_trait; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use const_format::concatcp; +use serde_json::Value; +use tokio::fs; + +use crate::plugins::_IN_GUEST_DEFAULT_KEY_PATH; +use crate::{Annotations, Decrypter, Encrypter, ProviderSettings}; +use crate::{Error, Result}; + +use super::annotations::EhsmProviderSettings; +use super::credential::Credential; + +pub struct EhsmKmsClient { + client: EHSMClient, +} + +const EHSM_IN_GUEST_DEFAULT_KEY_PATH: &str = concatcp!(_IN_GUEST_DEFAULT_KEY_PATH, "/ehsm"); + +impl EhsmKmsClient { + pub fn new(app_id: &str, api_key: &str, endpoint: &str) -> Result { + Ok(Self { + client: EHSMClient { + base_url: endpoint.to_owned(), + appid: app_id.to_owned(), + apikey: api_key.to_owned(), + }, + }) + } + + /// build client with parameters that have been exported to environment. + pub fn new_from_env() -> Result { + Ok(Self { + client: EHSMClient::new(), + }) + } + + /// This new function is used by a in-pod client. The side-effect is to read the + /// [`EHSM_IN_GUEST_DEFAULT_KEY_PATH`] which is the by default path where the credential + /// to access kms is saved. + pub async fn from_provider_settings(provider_settings: &ProviderSettings) -> Result { + let provider_settings: EhsmProviderSettings = + serde_json::from_value(Value::Object(provider_settings.clone())) + .map_err(|e| Error::EhsmKmsError(format!("parse provider setting failed: {e}")))?; + + let credential_path = format!( + "{EHSM_IN_GUEST_DEFAULT_KEY_PATH}/credential_{}.json", + provider_settings.app_id + ); + + let api_key = { + let cred = fs::read_to_string(credential_path) + .await + .map_err(|e| Error::EhsmKmsError(format!("read credential failed: {e}")))?; + let cred: Credential = serde_json::from_str(&cred) + .map_err(|e| Error::EhsmKmsError(format!("serialize credential failed: {e}")))?; + cred.api_key + }; + + Self::new( + &provider_settings.app_id, + &api_key, + &provider_settings.endpoint, + ) + } + + /// Export the [`ProviderSettings`] of the current client. This function is to be used + /// in the encryptor side. The [`ProviderSettings`] will be used to initial a client + /// in the decryptor side. + pub fn export_provider_settings(&self) -> Result { + let provider_settings = EhsmProviderSettings { + app_id: self.client.appid.clone(), + endpoint: self.client.base_url.clone(), + }; + + let provider_settings = serde_json::to_value(provider_settings) + .map_err(|e| Error::EhsmKmsError(format!("serialize ProviderSettings failed: {e}")))? + .as_object() + .expect("must be an object") + .to_owned(); + + Ok(provider_settings) + } +} + +#[async_trait] +impl Encrypter for EhsmKmsClient { + async fn encrypt(&mut self, data: &[u8], key_id: &str) -> Result<(Vec, Annotations)> { + let ciphertext = self + .client + .encrypt(key_id, &STANDARD.encode(data), None) + .await + .map_err(|e| Error::EhsmKmsError(format!("EHSM-KMS encrypt failed: {e}")))?; + + let annotations = Annotations::new(); + + Ok((ciphertext.into(), annotations)) + } +} + +#[async_trait] +impl Decrypter for EhsmKmsClient { + async fn decrypt( + &mut self, + ciphertext: &[u8], + key_id: &str, + _annotations: &Annotations, + ) -> Result> { + let plaintext_b64 = self + .client + .decrypt( + key_id, + std::str::from_utf8(ciphertext).map_err(|e| { + Error::EhsmKmsError(format!("decrypt &[u8] to &str failed: {e}")) + })?, + None, + ) + .await + .map_err(|e| Error::EhsmKmsError(format!("EHSM-KMS decrypt failed: {e}")))?; + let plaintext = STANDARD.decode(plaintext_b64).map_err(|e| { + Error::EhsmKmsError(format!("decode plaintext for decryption failed: {e}")) + })?; + + Ok(plaintext) + } +} + +impl EhsmKmsClient { + pub async fn create_key(&mut self, key_spec: &str) -> Result { + let origin = "EH_INTERNAL_KEY"; + let keyusage = "EH_KEYUSAGE_ENCRYPT_DECRYPT"; + let key_id = self + .client + .create_key(key_spec, origin, keyusage) + .await + .map_err(|e| Error::EhsmKmsError(format!("EHSM-KMS create key failed: {e}")))?; + + Ok(key_id) + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + use serde_json::json; + + use crate::{plugins::ehsm::client::EhsmKmsClient, Decrypter, Encrypter}; + + #[ignore] + #[tokio::test] + async fn test_create_key() { + let key_spec = "EH_AES_GCM_256"; + let provider_settings = json!({ + "app_id": "86f0e9fe-****-a224ddee1233", + "endpoint": "https://172.0.0.1:9000", + }); + + // init client at user side + let provider_settings = provider_settings.as_object().unwrap().to_owned(); + let mut client = EhsmKmsClient::from_provider_settings(&provider_settings) + .await + .unwrap(); + + // create key + let key_id = client.create_key(key_spec).await; + + assert!(key_id.is_ok()); + } + + #[rstest] + #[ignore] + #[case(b"this is a test plaintext")] + #[ignore] + #[case(b"this is a another test plaintext")] + #[tokio::test] + async fn key_lifetime(#[case] plaintext: &[u8]) { + let key_spec = "EH_AES_GCM_256"; + let provider_settings = json!({ + "app_id": "86f0e9fe-7f05-4110-9f65-a224ddee1233", + "endpoint": "https://172.16.1.1:9002", + }); + + // init client at user side + let provider_settings = provider_settings.as_object().unwrap().to_owned(); + let mut client = EhsmKmsClient::from_provider_settings(&provider_settings) + .await + .unwrap(); + + // create key + let key_id = client.create_key(key_spec).await.unwrap(); + + let mut encryptor = EhsmKmsClient::from_provider_settings(&provider_settings) + .await + .unwrap(); + + println!("{}", key_id); + + // do encryption + let (ciphertext, secret_settings) = encryptor + .encrypt(plaintext, &key_id) + .await + .expect("encrypt"); + let provider_settings = encryptor.export_provider_settings().unwrap(); + + // init decrypter in a guest + let mut decryptor = EhsmKmsClient::from_provider_settings(&provider_settings) + .await + .unwrap(); + + // do decryption + let decrypted = decryptor + .decrypt(&ciphertext, &key_id, &secret_settings) + .await + .expect("decrypt"); + + assert_eq!(decrypted, plaintext); + } +} diff --git a/confidential-data-hub/kms/src/plugins/ehsm/credential.rs b/confidential-data-hub/kms/src/plugins/ehsm/credential.rs new file mode 100644 index 000000000..707df1cc4 --- /dev/null +++ b/confidential-data-hub/kms/src/plugins/ehsm/credential.rs @@ -0,0 +1,16 @@ +// Copyright (c) 2023 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Credentials to access eHSM-KMS + +// use anyhow::*; +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +pub(crate) struct Credential { + pub _app_id: String, + pub api_key: String, +} diff --git a/confidential-data-hub/kms/src/plugins/ehsm/example_credential/credential.4eb1____.json b/confidential-data-hub/kms/src/plugins/ehsm/example_credential/credential.4eb1____.json new file mode 100644 index 000000000..355b8f638 --- /dev/null +++ b/confidential-data-hub/kms/src/plugins/ehsm/example_credential/credential.4eb1____.json @@ -0,0 +1,4 @@ +{ + "AppId": "4eb1****", + "ApiKey": "2LkL****" +} \ No newline at end of file diff --git a/confidential-data-hub/kms/src/plugins/ehsm/mod.rs b/confidential-data-hub/kms/src/plugins/ehsm/mod.rs new file mode 100644 index 000000000..2e85599ce --- /dev/null +++ b/confidential-data-hub/kms/src/plugins/ehsm/mod.rs @@ -0,0 +1,15 @@ +// Copyright (c) 2023 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! This is a eHSM KMS implementation. +//! +//! eHSM KMS uses eHSM-KMS to support all functions. +//! The project detail can be found here: . + +mod annotations; +mod client; +mod credential; + +pub use client::EhsmKmsClient; diff --git a/confidential-data-hub/kms/src/plugins/mod.rs b/confidential-data-hub/kms/src/plugins/mod.rs index defba9b11..36a740207 100644 --- a/confidential-data-hub/kms/src/plugins/mod.rs +++ b/confidential-data-hub/kms/src/plugins/mod.rs @@ -14,10 +14,16 @@ pub mod aliyun; pub mod kbs; +#[cfg(feature = "ehsm")] +pub mod ehsm; + #[derive(AsRefStr, EnumString)] pub enum DecryptorProvider { #[cfg(feature = "aliyun")] Aliyun, + + #[cfg(feature = "ehsm")] + Ehsm, } /// Create a new [`Decrypter`] by given provider name and [`ProviderSettings`] @@ -32,6 +38,11 @@ pub async fn new_decryptor( DecryptorProvider::Aliyun => Ok(Box::new( aliyun::AliyunKmsClient::from_provider_settings(&_provider_settings).await?, ) as Box), + + #[cfg(feature = "ehsm")] + DecryptorProvider::Ehsm => Ok(Box::new( + ehsm::EhsmKmsClient::from_provider_settings(&_provider_settings).await?, + ) as Box), } }