-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CDH: add en/decrypt support for eHSM-KMS
Signed-off-by: Daniel Duan <[email protected]>
- Loading branch information
1 parent
5df1101
commit d71874f
Showing
9 changed files
with
408 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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://<kms_ip>:<port>/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://<ip_addr>:<port> | ||
``` | ||
|
||
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Self> { | ||
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<Self> { | ||
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<Self> { | ||
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<ProviderSettings> { | ||
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<u8>, 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<Vec<u8>> { | ||
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<String> { | ||
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); | ||
} | ||
} |
Oops, something went wrong.