diff --git a/.github/workflows/kms.yml b/.github/workflows/kms.yml new file mode 100644 index 00000000..5ed677d1 --- /dev/null +++ b/.github/workflows/kms.yml @@ -0,0 +1,54 @@ +name: KMS CI +on: + push: + paths: + - 'deps/kms/**' + - '.github/workflows/kms.yml' + pull_request: + paths: + - 'deps/kms/**' + - '.github/workflows/kms.yml' + create: + paths: + - 'deps/kms/**' + - '.github/workflows/kms.yml' + +jobs: + crypto_ci: + if: github.event_name == 'pull_request' + name: Check + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + + steps: + - name: Code checkout + uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: Install Rust toolchain (stable) + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt + + - name: Run rust fmt check + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -p kms -- --check + + - name: Run rust lint check + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -p kms --all-features + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + args: -p kms --all-features diff --git a/Cargo.toml b/Cargo.toml index 53320fcc..8132c7d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ members = [ "deps/crypto", "deps/sev", "coco_keyprovider", - "test-binaries" + "test-binaries", + "deps/kms" ] [workspace.package] @@ -32,6 +33,7 @@ kbs-types = { git = "https://github.com/mkulke/kbs-types", branch = "mkulke/add- lazy_static = "1.4.0" log = "0.4.14" resource_uri = { path = "deps/resource_uri" } +rstest = "0.17.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" strum = { version = "0.24.0", features = ["derive"] } diff --git a/coco_keyprovider/Cargo.toml b/coco_keyprovider/Cargo.toml index e86111d9..38086b88 100644 --- a/coco_keyprovider/Cargo.toml +++ b/coco_keyprovider/Cargo.toml @@ -30,6 +30,6 @@ shadow-rs = "0.5.25" tonic-build = "0.5" [dev-dependencies] -rstest = "0.17.0" +rstest.workspace = true [features] diff --git a/deps/crypto/Cargo.toml b/deps/crypto/Cargo.toml index da5e1c42..6b8b55bf 100644 --- a/deps/crypto/Cargo.toml +++ b/deps/crypto/Cargo.toml @@ -19,7 +19,7 @@ rand = { version = "0.8.5" } sha2 = { version = "0.10" } [dev-dependencies] -rstest = "0.17.0" +rstest.workspace = true [features] default = ["rust-crypto"] diff --git a/deps/kms/Cargo.toml b/deps/kms/Cargo.toml new file mode 100644 index 00000000..a8e3dce3 --- /dev/null +++ b/deps/kms/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "kms" +version = "0.1.0" +authors = ["The Attestation Agent Authors"] +publish = false +edition = "2021" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +crypto = { path = "../crypto", default-features = false, optional = true} +hkdf = { version = "0.12.3", optional = true } +rand = { version = "0.8.4", optional = true } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +sha2 = { version = "0.10.6", optional = true } +strum.workspace = true +tokio = "1.0" +uuid = { version = "1.3.0", features = ["fast-rng", "v4"], optional = true } +zeroize = { version = "1.6.0", optional = true } + +[dev-dependencies] +rstest.workspace = true +tokio = { version = "1.0", features = ["rt", "macros" ] } + +[features] +default = [ "sample-rust" ] + +sample = [ "zeroize", "rand", "uuid", "hkdf", "sha2", "serde_json", "serde" ] +sample-openssl = [ "crypto/openssl", "sample" ] +sample-rust = [ "crypto/rust-crypto", "sample" ] diff --git a/deps/kms/src/api.rs b/deps/kms/src/api.rs new file mode 100644 index 00000000..1ca7a638 --- /dev/null +++ b/deps/kms/src/api.rs @@ -0,0 +1,29 @@ +// Copyright (c) 2023 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +use anyhow::Result; +use async_trait::async_trait; + +#[async_trait] +pub trait KMS { + /// Generate a key inside the KMS, and return an unique id string + /// that represents the key inside KMS. Then this unique id string + /// can be used to encrypt data slice or decrypt ciphertext that + /// has been encrypted with the key. + /// + /// Note: the returned string can only allow letters, numbers, + /// underscores `_` and hyphen `-` are allowed + async fn generate_key(&mut self) -> Result; + + /// Use the key of `keyid` to encrypt the `data` slice inside KMS, and then + /// return the ciphertext of the `data`. The encryption operation should occur + /// inside KMS. This function only works as a wrapper for different KMS APIs + async fn encrypt(&mut self, data: &[u8], keyid: &str) -> Result>; + + /// Use the key of `keyid` to decrypt the `ciphertext` slice inside KMS, and then + /// return the plaintext of the `data`. The decryption operation should occur + /// inside KMS. This function only works as a wrapper for different KMS APIs + async fn decrypt(&mut self, ciphertext: &[u8], keyid: &str) -> Result>; +} diff --git a/deps/kms/src/lib.rs b/deps/kms/src/lib.rs new file mode 100644 index 00000000..5bf5f1ba --- /dev/null +++ b/deps/kms/src/lib.rs @@ -0,0 +1,22 @@ +// Copyright (c) 2023 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! # KMS +//! +//! This lib defines the API of the KMSes which will be used to implement +//! encryption and decryption. +//! +//! ## Trait Definitions +//! +//! ### KMS +//! +//! [`KMS`] defines the interface of KMS. + +pub mod api; +pub use api::KMS; + +pub mod plugins; + +pub mod types; diff --git a/deps/kms/src/plugins/mod.rs b/deps/kms/src/plugins/mod.rs new file mode 100644 index 00000000..337765b6 --- /dev/null +++ b/deps/kms/src/plugins/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) 2023 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +pub mod sample; diff --git a/deps/kms/src/plugins/sample.rs b/deps/kms/src/plugins/sample.rs new file mode 100644 index 00000000..c823714b --- /dev/null +++ b/deps/kms/src/plugins/sample.rs @@ -0,0 +1,148 @@ +// Copyright (c) 2023 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! This is a sample KMS implementation example. +//! +//! This sample KMS uses a hardcoded [`ROOT_SECRET`] to work as the entropy source +//! of a fake "HSM". All keys, IV are derived from a uuid and the [`ROOT_SECRET`]. + +use std::collections::HashSet; + +use anyhow::*; +use async_trait::async_trait; +use crypto::WrapType; +use hkdf::Hkdf; +use rand::RngCore; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use zeroize::Zeroizing; + +use crate::KMS; + +/// This secret works as a fake HSM's entropy source inside the sample KMS, which is used +/// to derive keys. +pub const ROOT_SECRET: &[u8] = &[ + 217, 155, 119, 5, 176, 186, 122, 22, 130, 149, 179, 163, 54, 114, 112, 176, 221, 155, 55, 27, + 245, 20, 202, 139, 155, 167, 240, 163, 55, 17, 218, 234, +]; + +#[derive(Serialize, Deserialize)] +struct Ciphertext { + data: Vec, + iv: [u8; 12], +} + +/// A fake a KMS implementation +pub struct SampleKms { + /// storage for the generated keys + keys: HashSet, + root_secret: Vec, +} + +#[async_trait] +impl KMS for SampleKms { + async fn generate_key(&mut self) -> Result { + let keyid = loop { + let uuid = uuid::Uuid::new_v4().to_string(); + + match self.keys.insert(uuid.clone()) { + true => break uuid, + false => continue, + } + }; + + Ok(keyid) + } + + async fn encrypt(&mut self, data: &[u8], keyid: &str) -> Result> { + if !self.check_key_exist(keyid) { + bail!("keyid {keyid} not exists inside KMS"); + } + + let key = Zeroizing::new(self.hkdf(keyid.as_bytes())?.into()); + let mut iv = [0u8; 12]; + + // Although thread_rng is a CSPRNG, it still does not avoid some flaws. + // However, for a demo it is enough here. For more information: + // https://rust-random.github.io/book/guide-rngs.html#not-a-crypto-library + rand::thread_rng().fill_bytes(&mut iv); + let data = crypto::encrypt(key, data.into(), iv.into(), WrapType::Aes256Gcm.as_ref())?; + let cp = Ciphertext { data, iv }; + + Ok(serde_json::to_vec(&cp)?) + } + + async fn decrypt(&mut self, ciphertext: &[u8], keyid: &str) -> Result> { + if !self.check_key_exist(keyid) { + bail!("keyid {keyid} not exists inside KMS"); + } + + let cp: Ciphertext = serde_json::from_slice(ciphertext)?; + let key = Zeroizing::new(self.hkdf(keyid.as_bytes())?.into()); + crypto::decrypt(key, cp.data, cp.iv.into(), WrapType::Aes256Gcm.as_ref()) + } +} + +impl SampleKms { + /// Use [HKDF](https://tools.ietf.org/html/rfc5869) to derive a 256-bit + /// key for AES-256-GCM. The IKM is the secret of the [`SampleKms`] + fn hkdf(&self, salt: &[u8]) -> Result<[u8; 32]> { + let hk = Hkdf::::new(Some(salt), &self.root_secret); + let mut key = [0u8; 32]; + hk.expand(b"key", &mut key).map_err(|e| anyhow!("{}", e))?; + Ok(key) + } + + /// Check whether the key of given keyid exists inside the KMS. + fn check_key_exist(&self, keyid: &str) -> bool { + self.keys.contains(keyid) + } +} + +impl Default for SampleKms { + fn default() -> Self { + Self { + keys: Default::default(), + root_secret: ROOT_SECRET.into(), + } + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use crate::{plugins::sample::SampleKms, KMS}; + + #[rstest] + #[case(b"this is a test plaintext")] + #[case(b"this is a another test plaintext")] + #[tokio::test] + async fn key_lifetime(#[case] plaintext: &[u8]) { + let mut kms = SampleKms::default(); + let keyid = kms.generate_key().await.expect("generate key"); + let ciphertext = kms.encrypt(plaintext, &keyid).await.expect("encrypt"); + let decrypted = kms.decrypt(&ciphertext, &keyid).await.expect("decrypt"); + assert_eq!(decrypted, plaintext); + } + + #[tokio::test] + async fn encrypt_with_an_non_existent_key() { + let mut kms = SampleKms::default(); + let ciphertext = kms.encrypt(b"a test text", "an-non-existent-key-id").await; + assert!(ciphertext.is_err()) + } + + #[tokio::test] + async fn decrypt_with_an_non_existent_key() { + let mut kms = SampleKms::default(); + let keyid = kms.generate_key().await.expect("generate key"); + let ciphertext = kms.encrypt(b"a test text", &keyid).await.expect("encrypt"); + + // Use a fake key id to decrypt + let decrypted = kms.decrypt(&ciphertext, "an-non-existent-key-id").await; + assert!(decrypted.is_err()) + } +} diff --git a/deps/kms/src/types.rs b/deps/kms/src/types.rs new file mode 100644 index 00000000..0c843abd --- /dev/null +++ b/deps/kms/src/types.rs @@ -0,0 +1,25 @@ +// Copyright (c) 2023 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +use anyhow::Result; +use strum::{AsRefStr, EnumString}; + +use crate::KMS; + +#[derive(EnumString, AsRefStr)] +pub enum KMSTypes { + #[cfg(feature = "sample")] + #[strum(serialize = "sample")] + Sample, +} + +impl KMSTypes { + pub fn to_kms(&self) -> Result> { + match self { + #[cfg(feature = "sample")] + KMSTypes::Sample => Ok(Box::::default()), + } + } +} diff --git a/kbc/Cargo.toml b/kbc/Cargo.toml index 5bc40950..e052eaff 100644 --- a/kbc/Cargo.toml +++ b/kbc/Cargo.toml @@ -28,7 +28,7 @@ zeroize.workspace = true [dev-dependencies] tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] } -rstest = "0.16.0" +rstest.workspace = true [build-dependencies] tonic-build = { version = "0.8.0", optional = true }