From f1d86d18489ed9d0a6040692c1ed833d8aed1085 Mon Sep 17 00:00:00 2001 From: Tyler Fanelli Date: Mon, 10 Feb 2025 23:34:50 -0500 Subject: [PATCH] kbs/plugins: Add ID_KEY plugin The ID_KEY plugin creates a new key tied to a Trustee server instance and mostly pertains to workloads that want a key unwrapping as a result of a successful attestation. Admins can POST to the plugin giving a base64 encoded byte vector as the tag. This byte vector will then be encrypted with the key and returned to the admin. Upon successful attestation, attestation clients can then send the encrypted byte vector that was wrapped with the server's encryption key to the plugin via a GET request. The tag in this case must be a base64 encoding of the encrypted data's bytes. The data will then be decrypted and sent back to the attestation client. Signed-off-by: Tyler Fanelli --- Cargo.lock | 7 +- kbs/Cargo.toml | 3 + kbs/src/plugins/implementations/id_key.rs | 201 ++++++++++++++++++++++ kbs/src/plugins/implementations/mod.rs | 3 + kbs/src/plugins/plugin_manager.rs | 10 +- 5 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 kbs/src/plugins/implementations/id_key.rs diff --git a/Cargo.lock b/Cargo.lock index fe47adf88..44d4f7eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2880,6 +2880,7 @@ dependencies = [ "cryptoki", "derivative", "env_logger 0.10.2", + "generic-array", "jsonwebtoken", "jwt-simple", "kbs-types", @@ -2888,6 +2889,7 @@ dependencies = [ "log", "mobc", "openssl", + "p384", "prost", "rand", "reference-value-provider-service", @@ -2900,6 +2902,7 @@ dependencies = [ "semver", "serde", "serde_json", + "sha2", "strum", "tempfile", "thiserror 2.0.3", @@ -3514,9 +3517,9 @@ dependencies = [ [[package]] name = "p384" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ "ecdsa", "elliptic-curve", diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index 3ca6e0e25..649a8a17b 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -46,6 +46,7 @@ clap = { workspace = true, features = ["derive", "env"] } config.workspace = true cryptoki = { version = "0.8.0", optional = true } env_logger.workspace = true +generic-array = "0.14.7" jsonwebtoken = { workspace = true, default-features = false } jwt-simple.workspace = true kbs-types.workspace = true @@ -53,6 +54,7 @@ kms = { workspace = true, default-features = false } lazy_static = "1.4.0" log.workspace = true mobc = { version = "0.8.5", optional = true } +p384 = { version = "0.13.1", features = ["arithmetic", "ecdh"] } prost = { workspace = true, optional = true } rand = "0.8.5" regex = "1.11.1" @@ -63,6 +65,7 @@ scc = "2" semver = "1.0.16" serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +sha2 = "0.10.8" strum.workspace = true thiserror.workspace = true time = { version = "0.3.37", features = ["std"] } diff --git a/kbs/src/plugins/implementations/id_key.rs b/kbs/src/plugins/implementations/id_key.rs new file mode 100644 index 000000000..402eaff42 --- /dev/null +++ b/kbs/src/plugins/implementations/id_key.rs @@ -0,0 +1,201 @@ +// Copyright (c) 2025 by Red Hat. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use super::super::plugin_manager::ClientPlugin; +use actix_web::http::Method; +use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit}; +use anyhow::{anyhow, bail, Context, Result}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use generic_array::GenericArray; +use openssl::{ + pkey::Private, + rsa::{Padding, Rsa}, +}; +use p384::{ecdh, PublicKey, SecretKey}; +use serde::Deserialize; +use sha2::Sha256; + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct IdKeyConfig; + +pub struct IdKey { + idkey_non_hsm: Rsa, + ecdh_secret: SecretKey, +} + +impl TryFrom for IdKey { + type Error = anyhow::Error; + + fn try_from(_value: IdKeyConfig) -> anyhow::Result { + Self::new() + } +} + +#[async_trait::async_trait] +impl ClientPlugin for IdKey { + async fn handle( + &self, + _body: &[u8], + query: &str, + path: &str, + method: &Method, + ) -> Result> { + let path = path + .strip_prefix('/') + .context("accessed path is illegal, should start with '/'")?; + + match *method { + Method::POST => { + let plain = URL_SAFE_NO_PAD + .decode(path) + .context("invalid id-key path")?; + + self.encrypt(plain) + } + Method::GET => match path { + "ecdh-pub-sec1" => Ok(self.ecdh_pubkey_sec1()), + _ => { + let (public_key, iv) = ecdh_iv(query.to_string())?; + + let wrapped = URL_SAFE_NO_PAD.decode(path).context("invalid path")?; + let encrypted = self.ecdh_unwrap(wrapped, public_key, iv)?; + let decrypted = self.decrypt(encrypted)?; + + Ok(decrypted) + } + }, + _ => bail!("Illegal HTTP method. Only supports `GET` and `POST`"), + } + } + + async fn validate_auth( + &self, + _body: &[u8], + _query: &str, + _path: &str, + method: &Method, + ) -> Result { + match method { + &Method::POST => Ok(true), + _ => Ok(false), + } + } + + async fn encrypted( + &self, + _body: &[u8], + _query: &str, + _path: &str, + method: &Method, + ) -> Result { + match *method { + Method::GET => Ok(true), + _ => Ok(false), + } + } +} + +impl IdKey { + pub fn new() -> Result { + Ok(Self { + idkey_non_hsm: Rsa::generate(2048).context("unable to create ID key")?, + ecdh_secret: SecretKey::random(&mut rand::thread_rng()), + }) + } + + fn encrypt(&self, bytes: Vec) -> Result> { + let encrypted = { + let mut d = [0u8; 256]; + let sz = self + .idkey_non_hsm + .public_encrypt(&bytes, &mut d, Padding::PKCS1) + .context("unable to decrypt key")?; + + Vec::from(&d[..sz]) + }; + + Ok(URL_SAFE_NO_PAD.encode(encrypted).into()) + } + + fn ecdh_pubkey_sec1(&self) -> Vec { + self.ecdh_secret.public_key().to_sec1_bytes().to_vec() + } + + fn ecdh_unwrap( + &self, + wrapped: Vec, + ec_public_key: PublicKey, + iv: Vec, + ) -> Result> { + let shared = ecdh::diffie_hellman( + self.ecdh_secret.to_nonzero_scalar(), + ec_public_key.as_affine(), + ); + let mut sha_bytes = [0u8; 32]; + + let hkdf = shared.extract::(None); + if hkdf.expand(&[], &mut sha_bytes).is_err() { + return Err(anyhow!("unable to get ECDH shared SHA hash")); + } + + let key = Key::::from_slice(&sha_bytes); + let aes = Aes256Gcm::new(key); + + let encrypted = match aes.decrypt(GenericArray::from_slice(iv.as_slice()), wrapped.as_ref()) + { + Ok(e) => e, + Err(_) => { + return Err(anyhow!( + "unable to unwrap encrypted secret with shared AES-GCM key" + )) + } + }; + + Ok(encrypted) + } + + fn decrypt(&self, encrypted: Vec) -> Result> { + let mut d = [0u8; 256]; + let sz = self + .idkey_non_hsm + .private_decrypt(&encrypted, &mut d, Padding::PKCS1) + .context("unable to decrypt key")?; + + Ok(Vec::from(&d[..sz])) + } +} + +fn ecdh_iv(query: String) -> Result<(PublicKey, Vec)> { + let subs: Vec<&str> = query.split('&').collect(); + if subs.len() != 2 { + bail!("invalid query"); + } + + let public_key = { + let bytes = parse_val("ecdh-pubkey", &subs)?; + PublicKey::from_sec1_bytes(&bytes) + .context("public key cannot be derived from SEC1 bytes")? + }; + + let iv = parse_val("iv", &subs)?; + + Ok((public_key, iv)) +} + +fn parse_val(key: &str, subs: &Vec<&str>) -> Result> { + for substr in subs { + let kv: Vec<&str> = substr.split('=').collect(); + if kv.len() != 2 { + bail!("invalid query"); + } + + if kv[0] == key { + let bytes = URL_SAFE_NO_PAD.decode(kv[1]).context("invalid query")?; + + return Ok(bytes); + } + } + + Err(anyhow!("invalid query")) +} diff --git a/kbs/src/plugins/implementations/mod.rs b/kbs/src/plugins/implementations/mod.rs index 8bf856bbf..4bf1314f1 100644 --- a/kbs/src/plugins/implementations/mod.rs +++ b/kbs/src/plugins/implementations/mod.rs @@ -5,5 +5,8 @@ pub mod resource; pub mod sample; +pub mod id_key; + +pub use id_key::{IdKey, IdKeyConfig}; pub use resource::{RepositoryConfig, ResourceStorage}; pub use sample::{Sample, SampleConfig}; diff --git a/kbs/src/plugins/plugin_manager.rs b/kbs/src/plugins/plugin_manager.rs index f558aa64f..81932ccbf 100644 --- a/kbs/src/plugins/plugin_manager.rs +++ b/kbs/src/plugins/plugin_manager.rs @@ -8,7 +8,7 @@ use actix_web::http::Method; use anyhow::{Context, Error, Result}; use serde::Deserialize; -use super::{sample, RepositoryConfig, ResourceStorage}; +use super::{sample, IdKey, IdKeyConfig, RepositoryConfig, ResourceStorage}; type ClientPluginInstance = Arc; @@ -59,6 +59,9 @@ pub enum PluginsConfig { #[serde(alias = "resource")] ResourceStorage(RepositoryConfig), + + #[serde(alias = "id-key")] + IdKey(IdKeyConfig), } impl Display for PluginsConfig { @@ -66,6 +69,7 @@ impl Display for PluginsConfig { match self { PluginsConfig::Sample(_) => f.write_str("sample"), PluginsConfig::ResourceStorage(_) => f.write_str("resource"), + PluginsConfig::IdKey(_) => f.write_str("id-key"), } } } @@ -85,6 +89,10 @@ impl TryInto for PluginsConfig { .context("Initialize 'Resource' plugin failed")?; Arc::new(resource_storage) as _ } + PluginsConfig::IdKey(cfg) => { + let id_key = IdKey::try_from(cfg).context("Initialize 'ID_KEY' plugin failed")?; + Arc::new(id_key) as _ + } }; Ok(plugin)