Skip to content
This repository has been archived by the owner on Jul 2, 2023. It is now read-only.

KMS: add API definitions and crate for KMS integration #189

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
54 changes: 54 additions & 0 deletions .github/workflows/kms.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ members = [
"deps/crypto",
"deps/sev",
"coco_keyprovider",
"test-binaries"
"test-binaries",
"deps/kms"
]

[workspace.package]
Expand All @@ -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"] }
Expand Down
2 changes: 1 addition & 1 deletion coco_keyprovider/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ shadow-rs = "0.5.25"
tonic-build = "0.5"

[dev-dependencies]
rstest = "0.17.0"
rstest.workspace = true

[features]
2 changes: 1 addition & 1 deletion deps/crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
31 changes: 31 additions & 0 deletions deps/kms/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" ]
29 changes: 29 additions & 0 deletions deps/kms/src/api.rs
Original file line number Diff line number Diff line change
@@ -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<String>;

/// 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<Vec<u8>>;

/// 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<Vec<u8>>;
}
22 changes: 22 additions & 0 deletions deps/kms/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions deps/kms/src/plugins/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) 2023 Alibaba Cloud
//
// SPDX-License-Identifier: Apache-2.0
//

pub mod sample;
148 changes: 148 additions & 0 deletions deps/kms/src/plugins/sample.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
iv: [u8; 12],
}

/// A fake a KMS implementation
pub struct SampleKms {
/// storage for the generated keys
keys: HashSet<String>,
root_secret: Vec<u8>,
}

#[async_trait]
impl KMS for SampleKms {
async fn generate_key(&mut self) -> Result<String> {
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<Vec<u8>> {
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<Vec<u8>> {
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::<Sha256>::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())
}
}
25 changes: 25 additions & 0 deletions deps/kms/src/types.rs
Original file line number Diff line number Diff line change
@@ -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<Box<dyn KMS>> {
match self {
#[cfg(feature = "sample")]
KMSTypes::Sample => Ok(Box::<crate::plugins::sample::SampleKms>::default()),
}
}
}
2 changes: 1 addition & 1 deletion kbc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down