Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CDH: add en/decrypt support for eHSM-KMS #359

Merged
merged 1 commit into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion confidential-data-hub/kms/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
4 changes: 4 additions & 0 deletions confidential-data-hub/kms/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
116 changes: 116 additions & 0 deletions confidential-data-hub/kms/src/plugins/ehsm/README.md
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
```
13 changes: 13 additions & 0 deletions confidential-data-hub/kms/src/plugins/ehsm/annotations.rs
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,
}
226 changes: 226 additions & 0 deletions confidential-data-hub/kms/src/plugins/ehsm/client.rs
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the way to read credential. Currently we follow the steps:

  1. CDH get credential from the KBS
  2. CDH put the credential under /run/..
  3. CDH reads the credential under /run/..

@fitzthum once mentioned that this will expose the plaintext of credentials to the guest and suggest that the credentials be kept in the memory of the CDH. I think it is a good way, and the steps could be

  1. CDH get credential from the KBS
  2. CDH sets its own process's env the credential with specific keys
  3. Concrete KMS plugin reads the credential from the env

Which I'd like to put another PR and let's talk more about that then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to hear that. eHSM-KMS itself supports reading the credential from environment variables, which will make the transition easy to implement.

);

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as mentioned

});

// 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);
}
}
Loading
Loading