From fcc05f0c017666da9fd8250a7a1c48702e85faca Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Nov 2024 19:10:41 +0000 Subject: [PATCH 1/5] kbs: Add splitapi plugin This plugin generates credentials (keys and certificates) for both the API proxy server (required for kata-containers/kata-containers#9159 and kata-containers/kata-containers#9752) and the workload owner. This plugin also delivers the credentials to a sandbox (i.e., confidential PODs or VMs), specifically to the kata agent to initiate the SplitAPI proxy server so that a workload owner can communicate with the proxy server using a secure tunnel. The IPv4 address, name, and the ID of the sandbox must be provided in the query string to obtain the credential resources from the kbs. After receiving the credential request, the splitapi plugin will create a key pair for the server and client and sign them using the self-signed CA. The generated ca.crt, server.crt, and server.key are stored in a directory specific to the sandbox (the caller) and returned to the caller. In addition, ca.key, client.key, and client.crt are also generated and stored to that particular directory specific to the sandbox (i.e., caller). During the credential generation, a sandbox directory mapper creates a unique directory specific to the sandbox (i.e., the caller). The mapper creates the unique directory using the sandbox parameters passed in the query string. A mapping file is also maintained to store the mapping between the sandbox name and the unique directory created for the sandbox. The splitapi plugin itself is not initialized by default. To initialize it, you need to add 'splitapi' in the kbs-config.toml. Signed-off-by: Salman Ahmed --- Cargo.lock | 5 + .../implementations/splitapi/backend.rs | 77 ++++ .../implementations/splitapi/generator.rs | 350 ++++++++++++++++++ .../implementations/splitapi/manager.rs | 163 ++++++++ .../implementations/splitapi/mapper.rs | 133 +++++++ .../plugins/implementations/splitapi/mod.rs | 140 +++++++ kbs/src/plugins/plugin_manager.rs | 2 +- 7 files changed, 869 insertions(+), 1 deletion(-) create mode 100644 kbs/src/plugins/implementations/splitapi/backend.rs create mode 100644 kbs/src/plugins/implementations/splitapi/generator.rs create mode 100644 kbs/src/plugins/implementations/splitapi/manager.rs create mode 100644 kbs/src/plugins/implementations/splitapi/mapper.rs create mode 100644 kbs/src/plugins/implementations/splitapi/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c54e1eb982..0897bcb445 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3285,6 +3285,7 @@ dependencies = [ "serial_test", "sha2", "strum", + "serde_qs", "tempfile", "thiserror 2.0.17", "time", @@ -5596,7 +5597,11 @@ checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" dependencies = [ "percent-encoding", "serde", +<<<<<<< HEAD "thiserror 1.0.69", +======= + "thiserror", +>>>>>>> cd1ac3e (kbs: Add splitapi plugin) ] [[package]] diff --git a/kbs/src/plugins/implementations/splitapi/backend.rs b/kbs/src/plugins/implementations/splitapi/backend.rs new file mode 100644 index 0000000000..fe3d9842d6 --- /dev/null +++ b/kbs/src/plugins/implementations/splitapi/backend.rs @@ -0,0 +1,77 @@ +// Copyright (c) 2024 by IBM Corporation +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context, Result}; +use std::{ffi::OsString, sync::Arc}; +use serde::Deserialize; + +use super::manager; + + +pub const PLUGIN_NAME: &str = "splitapi"; + + +/// Services supported by the SplitAPI plugin +#[async_trait::async_trait] +pub trait SplitAPIBackend: Send + Sync { + /// Generate and obtain the credential for API Proxy server + async fn get_server_credential(&self, params: &SandboxParams) -> Result>; +} + +pub struct SplitAPI { + pub backend: Arc, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum SplitAPIConfig { + CertManager(manager::SplitAPIRepoDesc), +} + +impl Default for SplitAPIConfig { + fn default() -> Self { + Self::CertManager(manager::SplitAPIRepoDesc::default()) + } +} + +impl TryFrom for SplitAPI { + type Error = anyhow::Error; + + fn try_from(config: SplitAPIConfig) -> anyhow::Result { + match config { + SplitAPIConfig::CertManager(desc) => { + let backend = manager::CertManager::new(&desc) + .context("Failed to initialize Resource Storage")?; + Ok(Self { + backend: Arc::new(backend), + }) + } + } + } +} + +/// Parameters taken by the "splitapi" plugin to store the certificates +/// generated for the sandbox by combining the IP address, sandbox name, +/// sandbox ID to create an unique directory for the sandbox +#[derive(Debug, PartialEq, serde::Deserialize)] +pub struct SandboxParams { + pub id: String, + pub ip: String, + pub name: String, +} + +impl From<&SandboxParams> for Vec { + fn from(params: &SandboxParams) -> Self { + let mut v: Vec = Vec::new(); + + v.push("-id".into()); + v.push((¶ms.id).into()); + v.push("-name".into()); + v.push((¶ms.name).into()); + v.push("-ip".into()); + v.push((¶ms.ip.to_string()).into()); + + v + } +} diff --git a/kbs/src/plugins/implementations/splitapi/generator.rs b/kbs/src/plugins/implementations/splitapi/generator.rs new file mode 100644 index 0000000000..f23c09eebf --- /dev/null +++ b/kbs/src/plugins/implementations/splitapi/generator.rs @@ -0,0 +1,350 @@ +// Copyright (c) 2024 by IBM Corporation +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use openssl::rsa::Rsa; +use openssl::x509::{X509NameBuilder, X509Name, X509, X509ReqBuilder, X509Req}; +use openssl::x509::extension::BasicConstraints; +use openssl::x509::extension::KeyUsage; +use openssl::x509::extension::SubjectKeyIdentifier; +use openssl::x509::extension::AuthorityKeyIdentifier; +use openssl::pkey::PKey; +use openssl::x509::X509Builder; +use openssl::hash::MessageDigest; +use std::fs::File; +use std::io::Write; +use std::io::Read; +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use std::path::PathBuf; +use std::error::Error; +use anyhow::Result; + +use super::backend::SandboxParams; + + +pub const CA_KEY_FILENAME: &str = "ca.key"; +pub const CA_CRT_FILENAME: &str = "ca.pem"; +pub const CLIENT_KEY_FILENAME: &str = "client.key"; +pub const CLIENT_CSR_FILENAME: &str = "client.csr"; +pub const CLIENT_CRT_FILENAME: &str = "client.pem"; +pub const SERVER_KEY_FILENAME: &str = "server.key"; +pub const SERVER_CSR_FILENAME: &str = "server.csr"; +pub const SERVER_CRT_FILENAME: &str = "server.pem"; + +const CREDENTIAL_KEY_SIZE: u32 = 2048; + + +#[derive(Debug, serde::Serialize)] +pub struct ServerCredential { + pub key: Vec, + pub crt: Vec, + pub ca_crt: Vec, +} + + +/// Credentials (keys and certs for ca, server, and client) stored +/// in plugin_dir/sandbox-specific-directory +#[derive(Debug)] +pub struct CredentialBundle { + key_size: u32, + ca_key: PathBuf, + ca_crt: PathBuf, + client_key: PathBuf, + client_csr: PathBuf, + client_crt: PathBuf, + server_key: PathBuf, + server_csr: PathBuf, + server_crt: PathBuf +} + +impl CredentialBundle { + pub fn new(sandbox_dir: &PathBuf) -> Result { + let ca_key: PathBuf = sandbox_dir.as_path().join(CA_KEY_FILENAME); + let ca_crt: PathBuf = sandbox_dir.as_path().join(CA_CRT_FILENAME); + + let client_key: PathBuf = sandbox_dir.as_path().join(CLIENT_KEY_FILENAME); + let client_csr: PathBuf = sandbox_dir.as_path().join(CLIENT_CSR_FILENAME); + let client_crt: PathBuf = sandbox_dir.as_path().join(CLIENT_CRT_FILENAME); + + let server_key: PathBuf = sandbox_dir.as_path().join(SERVER_KEY_FILENAME); + let server_csr: PathBuf = sandbox_dir.as_path().join(SERVER_CSR_FILENAME); + let server_crt: PathBuf = sandbox_dir.as_path().join(SERVER_CRT_FILENAME); + + Ok(Self { + key_size: CREDENTIAL_KEY_SIZE, + ca_key, + ca_crt, + client_key, + client_csr, + client_crt, + server_key, + server_csr, + server_crt + }) + } + pub fn server_key(&self) -> &PathBuf { + &self.server_key + } + + pub fn server_crt(&self) -> &PathBuf { + &self.server_crt + } + + pub fn ca_crt(&self) -> &PathBuf { + &self.ca_crt + } + + /// Run several steps for generate all the keys and certificates + pub fn generate( + &self, + params: &SandboxParams, + ) -> Result<&Self> { + //let mut args: Vec = Vec::from(params); + log::info!("Params {:?}", params); + + match self.generate_private_key(&self.ca_key, self.key_size) { + Ok(_) => println!("CA key generation succeeded and saved to {}.", self.ca_key.display()), + Err(e) => eprintln!("CA key generation failed: {}", e), + } + + match self.generate_ca_cert(&self.ca_crt, &self.ca_key) { + Ok(_) => println!("CA self-signed certificate generated and saved to {}.", self.ca_crt.display()), + Err(e) => eprintln!("CA self-signed certificate generation failed: {}", e), + } + + match self.generate_private_key(&self.server_key, self.key_size) { + Ok(_) => println!("Server key generation succeeded and saved to {}.", self.server_key.display()), + Err(e) => eprintln!("Server key generation failed: {}", e), + } + + let server_common_name = "server"; + match self.generate_csr(&self.server_csr, &self.server_key, server_common_name) { + Ok(_) => println!("Server csr generation succeeded and saved to {}.", self.server_csr.display()), + Err(e) => eprintln!("Server csr generation failed: {}", e), + } + + match self.generate_cert(&self.server_crt, &self.server_csr, &self.ca_crt, &self.ca_key) { + Ok(_) => println!("Server cert generation succeeded and saved to {}.", self.server_crt.display()), + Err(e) => eprintln!("Server cert generation failed: {}", e), + } + + match self.generate_private_key(&self.client_key, self.key_size) { + Ok(_) => println!("Client key generation succeeded and saved to {}.", self.client_key.display()), + Err(e) => eprintln!("Client key generation failed: {}", e), + } + + let client_common_name = "client"; + match self.generate_csr(&self.client_csr, &self.client_key, client_common_name) { + Ok(_) => println!("Client CSR generation succeeded and saved to {}.", self.client_csr.display()), + Err(e) => eprintln!("Client CSR generation failed: {}", e), + } + + match self.generate_cert(&self.client_crt, &self.client_csr, &self.ca_crt, &self.ca_key) { + Ok(_) => println!("Client cert generation succeeded and saved to {}.", self.client_crt.display()), + Err(e) => eprintln!("Client cert generation failed: {}", e), + } + + Ok(self) + } + + fn generate_private_key( + &self, + ca_key_path: &PathBuf, + key_size: u32 + ) -> Result<(), Box> { + // Generate RSA key + let rsa = Rsa::generate(key_size).expect("Failed to generate RSA key"); + let pkey = PKey::from_rsa(rsa).expect("Failed to create PKey from RSA"); + + // Write the private key to a file + let private_key_pem = pkey.private_key_to_pem_pkcs8()?; + let mut file = File::create(ca_key_path.as_path())?; + file.write_all(&private_key_pem)?; + + Ok(()) + } + + fn build_x509_name( + &self, + common_name: &str + ) -> Result> { + // Define certificate details + let country = "AA"; + let state = "Default State"; + let locality = "Default City"; + let organization = "Default Organization"; + let org_unit = "Default Unit"; + + // Build X.509 name + let mut name_builder = X509NameBuilder::new()?; + name_builder.append_entry_by_text("C", country)?; + name_builder.append_entry_by_text("ST", state)?; + name_builder.append_entry_by_text("L", locality)?; + name_builder.append_entry_by_text("O", organization)?; + name_builder.append_entry_by_text("OU", org_unit)?; + name_builder.append_entry_by_text("CN", common_name)?; + let name = name_builder.build(); + + Ok(name) + } + + fn generate_ca_cert( + &self, + crt_path: &PathBuf, + ca_key_path: &PathBuf + ) -> Result<(), Box> { + // Read the private key from file + let mut file = File::open(ca_key_path.as_path())?; + let mut key_pem = Vec::new(); + file.read_to_end(&mut key_pem)?; + let rsa = Rsa::private_key_from_pem(&key_pem)?; + let pkey = PKey::from_rsa(rsa)?; + + // Build X.509 name + let common_name = "grpc-tls CA"; + let name = self.build_x509_name(common_name)?; + + // Build the X.509 certificate + let mut x509_builder = X509Builder::new()?; + x509_builder.set_subject_name(&name)?; + x509_builder.set_issuer_name(&name)?; + x509_builder.set_pubkey(&pkey)?; + + // Set certificate validity period + x509_builder.set_not_before( + &Asn1Time::days_from_now(0).expect("Failed to set not before") + ).expect("Failed to set not before"); + x509_builder.set_not_after( + &Asn1Time::days_from_now(3650).expect("Failed to set not after") + ).expect("Failed to set not after"); + + // Sign the certificate + x509_builder.sign(&pkey, MessageDigest::sha256())?; + let x509 = x509_builder.build(); + + // Write the certificate to a file + let crt_pem = x509.to_pem()?; + let mut crt_file = File::create(crt_path.as_path())?; + crt_file.write_all(&crt_pem)?; + + Ok(()) + } + + fn generate_csr( + &self, + csr_path: &PathBuf, + private_key_path: &PathBuf, + common_name: &str + ) -> Result<(), Box> { + + // Read the private key from file + let mut file = File::open(private_key_path.as_path())?; + let mut key_pem = Vec::new(); + file.read_to_end(&mut key_pem)?; + let rsa = Rsa::private_key_from_pem(&key_pem)?; + let pkey = PKey::from_rsa(rsa)?; + + // Build X.509 name + let name = self.build_x509_name(common_name)?; + + // Create a new X.509 certificate signing request (CSR) + let mut csr_builder = X509ReqBuilder::new()?; + csr_builder.set_subject_name(&name)?; + csr_builder.set_pubkey(&pkey)?; + csr_builder.sign(&pkey, MessageDigest::sha256())?; + + let csr = csr_builder.build(); + + // Write CSR to a file + let mut csr_file = File::create(csr_path.as_path())?; + csr_file.write_all(&csr.to_pem()?)?; + + Ok(()) + } + + fn generate_cert( + &self, + crt_path: &PathBuf, + csr_path: &PathBuf, + ca_crt_path: &PathBuf, + ca_key_path: &PathBuf + ) -> Result<(), Box> { + // Step 1: Read the CSR + let mut csr_file = File::open(csr_path.as_path())?; + let mut csr_data = vec![]; + csr_file.read_to_end(&mut csr_data)?; + let csr = X509Req::from_pem(&csr_data)?; + + // Step 2: Read the CA PEM + let mut ca_file = File::open(ca_crt_path.as_path())?; + let mut ca_data = vec![]; + ca_file.read_to_end(&mut ca_data)?; + let ca_cert = X509::from_pem(&ca_data)?; + + // Step 3: Read the CA Key + let mut ca_key_file = File::open(ca_key_path.as_path())?; + let mut ca_key_data = vec![]; + ca_key_file.read_to_end(&mut ca_key_data)?; + let ca_key = PKey::private_key_from_pem(&ca_key_data)?; + + // Step 5: Create the server certificate + let mut builder = X509Builder::new()?; + + // Set the version of the certificate + builder.set_version(2)?; + + // Set the serial number + let serial_number = { + let mut serial = BigNum::new()?; + serial.rand(159, openssl::bn::MsbOption::MAYBE_ZERO, false)?; + serial.to_asn1_integer()? + }; + builder.set_serial_number(&serial_number)?; + + // Set the subject name from the CSR + builder.set_subject_name(csr.subject_name())?; + //TODO: add sandbox IP in the subject + + // Set the issuer name from the CA certificate + builder.set_issuer_name(ca_cert.subject_name())?; + + // Set the public key from the CSR + let public_key = csr.public_key()?; + builder.set_pubkey(&public_key)?; + + // Set the certificate validity period + let not_before = openssl::asn1::Asn1Time::days_from_now(0)?; + let not_after = openssl::asn1::Asn1Time::days_from_now(3650)?; + builder.set_not_before(¬_before)?; + builder.set_not_after(¬_after)?; + + // Add extensions from the certificate extensions file + builder.append_extension(BasicConstraints::new().critical().build()?)?; + builder.append_extension( + KeyUsage::new() + .digital_signature() + .key_encipherment().build()? + )?; + builder.append_extension( + SubjectKeyIdentifier::new() + .build(&builder.x509v3_context(None, None))? + )?; + builder.append_extension( + AuthorityKeyIdentifier::new() + .keyid(false) + .issuer(false) + .build(&builder.x509v3_context(Some(&ca_cert), None))? + )?; + + // Sign the certificate with the CA key + builder.sign(&ca_key, MessageDigest::sha256())?; + + // Write the server certificate to a file + let server_crt = builder.build().to_pem()?; + let mut crt_file = File::create(crt_path.as_path())?; + crt_file.write_all(&server_crt)?; + + Ok(()) + } +} \ No newline at end of file diff --git a/kbs/src/plugins/implementations/splitapi/manager.rs b/kbs/src/plugins/implementations/splitapi/manager.rs new file mode 100644 index 0000000000..e4c3fbc5cf --- /dev/null +++ b/kbs/src/plugins/implementations/splitapi/manager.rs @@ -0,0 +1,163 @@ +// Copyright (c) 2024 by IBM Corporation +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{anyhow, Context, Result}; +use std::{sync::Arc}; +use serde::Deserialize; +use std::{ + fs, + path::{Path, PathBuf}, +}; +use std::sync::Mutex; +use lazy_static::lazy_static; + +use super::backend::{SplitAPIBackend, SandboxParams}; +use super::mapper::{SandboxDirectoryMapper, SandboxDirectoryInfo}; +use super::generator::{CredentialBundle, ServerCredential}; + + +pub const DEFAULT_PLUGIN_DIR: &str = "/opt/confidential-containers/kbs/plugin/splitapi"; +pub const SANDBOX_DIRECTORY_MAPPING_FILENAME: &str = "sandbox-credential-mapping.json"; + + +// Use lazy_static to initialize the SANDBOX_DIRECTORY_MANAGER only once +lazy_static! { + static ref SANDBOX_DIRECTORY_MAPPER: Arc>> = Arc::new(Mutex::new(None)); +} + +// Initialize the singleton with the provided file path +fn init_sandbox_directory_mapper(file_path: PathBuf) -> std::io::Result<()> { + let mut mapper = SANDBOX_DIRECTORY_MAPPER.lock().unwrap(); + + // Attempt to load the DirectoryManager from the file + match SandboxDirectoryMapper::load_from_file(file_path) { + Ok(loaded_mapper) => { + *mapper = Some(loaded_mapper); + } + Err(_e) => { + // Initialize a new manager + *mapper = Some(SandboxDirectoryMapper::new()); + + // TODO: check specific errors (file not found or something else) + // and handle those specific errors + // bail if there's relevant condition + } + } + + Ok(()) +} + +// Get a reference to the singleton +fn get_sandbox_directory_mapper() -> Arc>> { + Arc::clone(&SANDBOX_DIRECTORY_MAPPER) +} + + +#[derive(Debug, Deserialize, Clone, PartialEq)] +pub struct SplitAPIRepoDesc { + #[serde(default)] + pub plugin_dir: String, +} + +impl Default for SplitAPIRepoDesc { + fn default() -> Self { + Self { + plugin_dir: DEFAULT_PLUGIN_DIR.into(), + } + } +} + + +pub struct CertManager { + pub plugin_dir: String, + pub mapping_filename: String, + mapper: Arc>>, +} + + +impl CertManager { + pub fn new(repo_desc: &SplitAPIRepoDesc) -> anyhow::Result { + // Create splitapi_res work dir. + if !Path::new(&repo_desc.plugin_dir).exists() { + fs::create_dir_all(&repo_desc.plugin_dir)?; + + log::info!("Splitapi plugin directory created = {}", repo_desc.plugin_dir); + } + + // Initialize directory manager with the content from a file + let mapping_file: PathBuf = PathBuf::from(&repo_desc.plugin_dir) + .as_path() + .join(SANDBOX_DIRECTORY_MAPPING_FILENAME + ); + init_sandbox_directory_mapper(mapping_file.clone())?; + log::info!("Directory manager loaded the data from file: {}", mapping_file.display()); + + // Initialize the manager + Ok(Self { + plugin_dir: repo_desc.plugin_dir.clone(), + mapping_filename: SANDBOX_DIRECTORY_MAPPING_FILENAME.into(), + mapper: get_sandbox_directory_mapper(), + }) + } +} + +#[async_trait::async_trait] +impl SplitAPIBackend for CertManager { + async fn get_server_credential(&self, params: &SandboxParams) -> Result> { + // Try locking the sandbox directory mapper + let mut mapper_guard = self.mapper.lock().map_err(|e| { + anyhow!("Failed to lock sandbox directory mapper: {}", e) + })?; + + if let Some(mapper) = mapper_guard.as_mut() { + let sandbox_dir_info: SandboxDirectoryInfo; + + if let Some(existing_dir) = mapper.get_directory(¶ms.name) { + + log::info!("Found existing directory: {:?}", existing_dir.sandbox_dir()); + sandbox_dir_info = existing_dir.clone(); + + //TODO: check if the credentails are already in there + // send the existing credentials if they are not expired + } else { + let new_dir_info = mapper.create_directory( + Path::new(&self.plugin_dir), + ¶ms + )?; + log::info!("New directory created: {:?}", new_dir_info); + + let mapping_file = PathBuf::from(&self.plugin_dir) + .as_path() + .join(&self.mapping_filename); + + mapper.write_to_file( + &new_dir_info, + &mapping_file + )?; + + sandbox_dir_info = new_dir_info; + } + + // Generate the credentials (keys and certs for ca, server, and client) + let cred_bundle = CredentialBundle::new(sandbox_dir_info.sandbox_dir())?; + cred_bundle.generate(params)?; + + // Return the server specific credentials + let resource = ServerCredential { + key: fs::read(cred_bundle.server_key().as_path()) + .with_context(|| format!("read {}", cred_bundle.server_key().display()))?, + crt: fs::read(cred_bundle.server_crt().as_path()) + .with_context(|| format!("read {}", cred_bundle.server_crt().display()))?, + ca_crt: fs::read(cred_bundle.ca_crt().as_path()) + .with_context(|| format!("read {}", cred_bundle.ca_crt().display()))?, + }; + + Ok(serde_json::to_vec(&resource)?) + + } else { + // Handle the case where the manager is None + Err(anyhow!("Directory manager is uninitialized")) + } + } +} \ No newline at end of file diff --git a/kbs/src/plugins/implementations/splitapi/mapper.rs b/kbs/src/plugins/implementations/splitapi/mapper.rs new file mode 100644 index 0000000000..f3b5272700 --- /dev/null +++ b/kbs/src/plugins/implementations/splitapi/mapper.rs @@ -0,0 +1,133 @@ +// Copyright (c) 2024 by IBM Corporation +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use std::fs::File; +use std::io::Write; +use std::path::{Path, PathBuf}; +use anyhow::{Context, Result}; +use std::io; +use std::fs; +use std::collections::HashMap; +use std::fs::OpenOptions; +use std::io::{BufRead, BufReader}; + +use super::backend::SandboxParams; + + +/// It has the fields to store the mapping between a sandbox name or id +/// to a unique directory created by the directory manager +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SandboxDirectoryInfo { + id: String, + ip: String, + name: String, + sandbox_dir: PathBuf, +} + +impl SandboxDirectoryInfo { + pub fn sandbox_dir(&self) -> &PathBuf { + &self.sandbox_dir + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct SandboxDirectoryMapper { + // Maps sandbox name to SandboxDirectoryInfo + sandbox_directory_mapper: HashMap, +} + + +/// Responsible for generating, storing, or loading unique directory +/// names for each sandbox. That means it creates a unique directory +/// for a sandbox (if the directory does not already exist), and stores +/// the mapping between the sandbox name (or id) to a file +impl SandboxDirectoryMapper { + pub fn new() -> Self { + SandboxDirectoryMapper { + sandbox_directory_mapper: HashMap::new(), + } + } + + // Generate a unique directory name from the fields + fn generate_unique_dirname(id: &str, ip: &str, name: &str) -> String { + format!("{}_{}_{}", name, ip, id) + } + + // Create a directory and store it in the HashMap + pub fn create_directory( + &mut self, + plugin_dir: &Path, + params: &SandboxParams + ) -> Result { + + let directory_name = SandboxDirectoryMapper::generate_unique_dirname( + ¶ms.id, + ¶ms.ip, + ¶ms.name + ); + + let directory_path: PathBuf = PathBuf::from(plugin_dir).as_path().join(&directory_name); + + // Create the directory + fs::create_dir_all(&directory_path.clone()) + .with_context(|| format!("Create {} dir", directory_path.display()))?; + + log::info!("Directory {} created", directory_name); + + // Store directory info in the HashMap + let dir_info = SandboxDirectoryInfo { + id: params.id.clone(), + ip: params.ip.clone(), + name: params.name.clone(), + sandbox_dir: directory_path.clone(), + }; + + self.sandbox_directory_mapper.insert(params.name.clone(), dir_info.clone()); + + Ok(dir_info) + } + + // Retrieve the directory info by name + pub fn get_directory(&self, name: &str) -> Option<&SandboxDirectoryInfo> { + self.sandbox_directory_mapper.get(name) + } + + // Function to write SandboxDirectoryInfo to a JSON file + pub fn write_to_file( + &self, + dir_info: &SandboxDirectoryInfo, + file_path: &PathBuf + ) -> io::Result<()> { + + let file = OpenOptions::new().append(true).create(true).open(file_path)?; + let mut writer = std::io::BufWriter::new(file); + + // Serialize the SandboxDirectoryInfo entry and append it to the file with a newline delimiter + serde_json::to_writer(&mut writer, &dir_info)?; + writer.write_all(b"\n")?; // Add a newline to separate entries + + Ok(()) + } + + // Load the directory data from a JSON file + pub fn load_from_file(file_path: PathBuf) -> Result { + + log::info!("Loading directory info: {}", file_path.display()); + + let file = File::open(file_path)?; + let reader = BufReader::new(file); + + // Create a new SandboxDirectoryMapper and populate its HashMap + let mut mapper = SandboxDirectoryMapper::new(); + + for line in reader.lines() { + let line = line?; + let entry: SandboxDirectoryInfo = serde_json::from_str(&line)?; + log::info!("{:?}", entry); + mapper.sandbox_directory_mapper.insert(entry.name.clone(), entry); + } + + Ok(mapper) + } +} \ No newline at end of file diff --git a/kbs/src/plugins/implementations/splitapi/mod.rs b/kbs/src/plugins/implementations/splitapi/mod.rs new file mode 100644 index 0000000000..223185132b --- /dev/null +++ b/kbs/src/plugins/implementations/splitapi/mod.rs @@ -0,0 +1,140 @@ +// Copyright (c) 2024 by IBM Corporation +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +//! Splitapi plugin provisions credential resources for a sandbox and sends +//! sever specific credentials to the sandbox to initiate Split API proxy +//! server and establish a secure tunnel between tenant and the API proxy +//! server. + +pub mod manager; +pub mod mapper; +pub mod generator; + +use actix_web::http::Method; +use anyhow::{anyhow, Error, bail, Result}; + +pub mod backend; +pub use backend::*; + +use super::super::plugin_manager::ClientPlugin; + + +#[async_trait::async_trait] +impl ClientPlugin for SplitAPI { + async fn handle( + &self, + _body: &[u8], + query: &str, + path: &str, + method: &Method, + ) -> Result> { + if method.as_str() != "GET" { + bail!("Illegal HTTP method**. Only supports `GET`") + } + + match path { + "credential" => { + let params: SandboxParams = + serde_qs::from_str(query).map_err(|e| { + anyhow!("Failed to parse query string: {}", e) + })?; + let credential = self.backend.get_server_credential(¶ms).await?; + + Ok(credential) + } + _ => Err(Error::msg("Illegal format of the request")) + } + } + + async fn validate_auth( + &self, + _body: &[u8], + _query: &str, + _path: &str, + _method: &Method, + ) -> Result { + Ok(false) + } + + /// Whether the body needs to be encrypted via TEE key pair. + /// If returns `Ok(true)`, the KBS server will encrypt the whole body + /// with TEE key pair and use KBS protocol's Response format. + async fn encrypted( + &self, + _body: &[u8], + _query: &str, + _path: &str, + _method: &Method, + ) -> Result { + Ok(false) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use tokio; + use std::sync::Arc; + use std::{ + fs, + path::{PathBuf}, + }; + use anyhow::Context; + + use super::generator::{CA_CRT_FILENAME, SERVER_KEY_FILENAME, SERVER_CRT_FILENAME}; + use super::generator::ServerCredential; + + + #[tokio::test] + async fn test_handle() { + // Arrange: create an instance of `SplitAPI` + let desc = manager::SplitAPIRepoDesc::default(); + let backend = manager::CertManager::new(&desc); + let backend = Arc::new(backend.expect("Failed to initialize backend")); + let split_api = SplitAPI { backend: backend }; + + // Define sample inputs + let body: &[u8] = b""; + let query = "id=3367348&ip=60.11.12.43&name=pod7"; + let path = "credential"; + let method = &Method::GET; + + // Act: call the handle method + let result = split_api.handle(body, query, path, method).await; + + println!("plugin dir = {}", desc.plugin_dir); + + // Assert: check the result + match result { + Ok(response) => { + + // Expected results + let sandbox_dir = PathBuf::from(&desc.plugin_dir) + .as_path() + .join("pod7_60.11.12.43_3367348"); + + let server_key = sandbox_dir.as_path().join(SERVER_KEY_FILENAME); + let server_crt = sandbox_dir.as_path().join(SERVER_CRT_FILENAME); + let ca_crt = sandbox_dir.as_path().join(CA_CRT_FILENAME); + + let resource = ServerCredential { + key: fs::read(server_key.as_path()) + .with_context(|| format!("read {}", server_key.display())) + .expect("failed to read server key"), + crt: fs::read(server_crt.as_path()) + .with_context(|| format!("read {}", server_crt.display())) + .expect("failed to read server crt"), + ca_crt: fs::read(ca_crt.as_path()) + .with_context(|| format!("read {}", ca_crt.display())) + .expect("failed to read ca crt"), + }; + + let expected_response = serde_json::to_vec(&resource).unwrap(); + assert_eq!(response, expected_response); + } + Err(e) => panic!("Expected Ok, got Err: {:?}", e), + } + } +} \ No newline at end of file diff --git a/kbs/src/plugins/plugin_manager.rs b/kbs/src/plugins/plugin_manager.rs index c4d91ca552..6702743a00 100644 --- a/kbs/src/plugins/plugin_manager.rs +++ b/kbs/src/plugins/plugin_manager.rs @@ -147,4 +147,4 @@ impl PluginManager { pub fn get(&self, name: &str) -> Option { self.plugins.get(name).cloned() } -} +} \ No newline at end of file From ceacef87a5dd97025c1d610900db19a82394e8dd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 18 Jan 2025 06:04:49 +0000 Subject: [PATCH 2/5] kbs: Update splitapi plugin Updates include the following changes: - removing the storage of credentials on the filesystem, - serializing/desrializing the credentials dictionary as needed. - configurable credentials/certificate details - other coding related issues Signed-off-by: Salman Ahmed --- Cargo.lock | 1 + kbs/Cargo.toml | 2 + kbs/Makefile | 3 + kbs/src/plugins/implementations/mod.rs | 4 + .../implementations/splitapi/backend.rs | 72 +-- .../implementations/splitapi/generator.rs | 455 ++++++++++-------- .../implementations/splitapi/manager.rs | 251 +++++----- .../implementations/splitapi/mapper.rs | 133 ----- .../plugins/implementations/splitapi/mod.rs | 90 ++-- kbs/src/plugins/plugin_manager.rs | 14 +- 10 files changed, 468 insertions(+), 557 deletions(-) delete mode 100644 kbs/src/plugins/implementations/splitapi/mapper.rs diff --git a/Cargo.lock b/Cargo.lock index 0897bcb445..8326da75b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3286,6 +3286,7 @@ dependencies = [ "sha2", "strum", "serde_qs", + "strum", "tempfile", "thiserror 2.0.17", "time", diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index f531006124..0e493baf4f 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -36,6 +36,8 @@ pkcs11 = ["cryptoki"] # Use Nebula Certificate Authority plugin to provide CA services to nodes # that want to join a Nebula overlay network nebula-ca-plugin = [] +# Use SplitAPI plugin to provide credentials for sandbox (pods) to initiate API proxy server +splitapi-plugin = [] # Use HashiCorp Vault KV v1 as KBS backend vault = ["vaultrs"] diff --git a/kbs/Makefile b/kbs/Makefile index ffea7970b7..b311d0c32b 100644 --- a/kbs/Makefile +++ b/kbs/Makefile @@ -2,6 +2,7 @@ AS_TYPE ?= coco-as ALIYUN ?= false NEBULA_CA_PLUGIN ?= false VAULT ?= false +SPLITAPI_PLUGIN ?= false BUILD_ARCH := $(shell uname -m) ARCH ?= $(shell uname -m) @@ -54,6 +55,8 @@ endif ifeq ($(NEBULA_CA_PLUGIN), true) FEATURES += nebula-ca-plugin +ifeq ($(SPLITAPI_PLUGIN), true) + FEATURES += splitapi-plugin endif ifeq ($(VAULT), true) diff --git a/kbs/src/plugins/implementations/mod.rs b/kbs/src/plugins/implementations/mod.rs index 7e72422f33..d3285939eb 100644 --- a/kbs/src/plugins/implementations/mod.rs +++ b/kbs/src/plugins/implementations/mod.rs @@ -8,6 +8,8 @@ pub mod nebula_ca; pub mod pkcs11; pub mod resource; pub mod sample; +#[cfg(feature = "splitapi-plugin")] +pub mod splitapi; #[cfg(feature = "nebula-ca-plugin")] pub use nebula_ca::{NebulaCaPlugin, NebulaCaPluginConfig}; @@ -15,3 +17,5 @@ pub use nebula_ca::{NebulaCaPlugin, NebulaCaPluginConfig}; pub use pkcs11::{Pkcs11Backend, Pkcs11Config}; pub use resource::{RepositoryConfig, ResourceStorage}; pub use sample::{Sample, SampleConfig}; +#[cfg(feature = "splitapi-plugin")] +pub use splitapi::{SplitAPI, SplitAPIConfig}; diff --git a/kbs/src/plugins/implementations/splitapi/backend.rs b/kbs/src/plugins/implementations/splitapi/backend.rs index fe3d9842d6..d9f5cdc246 100644 --- a/kbs/src/plugins/implementations/splitapi/backend.rs +++ b/kbs/src/plugins/implementations/splitapi/backend.rs @@ -1,21 +1,20 @@ -// Copyright (c) 2024 by IBM Corporation +// Copyright (c) 2025 by IBM Corporation // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use anyhow::{Context, Result}; -use std::{ffi::OsString, sync::Arc}; +use anyhow::Result; use serde::Deserialize; +use std::{path::PathBuf, sync::Arc}; +use super::generator::CertificateDetails; use super::manager; - -pub const PLUGIN_NAME: &str = "splitapi"; - +pub const CREDENTIALS_BLOB_FILE: &str = "certificates.json"; /// Services supported by the SplitAPI plugin #[async_trait::async_trait] pub trait SplitAPIBackend: Send + Sync { - /// Generate and obtain the credential for API Proxy server + /// Returns credentials for API Proxy server, generates if not exist async fn get_server_credential(&self, params: &SandboxParams) -> Result>; } @@ -25,13 +24,21 @@ pub struct SplitAPI { #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(tag = "type")] -pub enum SplitAPIConfig { - CertManager(manager::SplitAPIRepoDesc), +pub struct SplitAPIConfig { + pub plugin_dir: String, + #[serde(default)] + pub credential_blob_filename: String, + #[serde(default)] + pub certificate_details: CertificateDetails, } impl Default for SplitAPIConfig { fn default() -> Self { - Self::CertManager(manager::SplitAPIRepoDesc::default()) + Self { + plugin_dir: String::from(""), + credential_blob_filename: CREDENTIALS_BLOB_FILE.into(), + certificate_details: CertificateDetails::default(), + } } } @@ -39,39 +46,38 @@ impl TryFrom for SplitAPI { type Error = anyhow::Error; fn try_from(config: SplitAPIConfig) -> anyhow::Result { - match config { - SplitAPIConfig::CertManager(desc) => { - let backend = manager::CertManager::new(&desc) - .context("Failed to initialize Resource Storage")?; - Ok(Self { - backend: Arc::new(backend), - }) - } - } + let backend = manager::CertManager::new( + PathBuf::from(&config.plugin_dir), + config.credential_blob_filename, + &config.certificate_details, + )?; + + Ok(Self { + backend: Arc::new(backend), + }) } } -/// Parameters taken by the "splitapi" plugin to store the certificates -/// generated for the sandbox by combining the IP address, sandbox name, -/// sandbox ID to create an unique directory for the sandbox +/// Parameters for the credential request +/// +/// These parameters are provided in the request via URL query string. +/// Parameters taken by the "splitapi" plugin to generate a unique key +/// for a sandbox store and retrieve credentials specific to the sandbox. #[derive(Debug, PartialEq, serde::Deserialize)] pub struct SandboxParams { + /// Required: ID of a sandbox or pod pub id: String, + // Required: IP of a sandbox or pod pub ip: String, + // Required: name of a sandbox or pod pub name: String, } -impl From<&SandboxParams> for Vec { - fn from(params: &SandboxParams) -> Self { - let mut v: Vec = Vec::new(); - - v.push("-id".into()); - v.push((¶ms.id).into()); - v.push("-name".into()); - v.push((¶ms.name).into()); - v.push("-ip".into()); - v.push((¶ms.ip.to_string()).into()); +impl TryFrom<&str> for SandboxParams { + type Error = anyhow::Error; - v + fn try_from(query: &str) -> Result { + let params: SandboxParams = serde_qs::from_str(query)?; + Ok(params) } } diff --git a/kbs/src/plugins/implementations/splitapi/generator.rs b/kbs/src/plugins/implementations/splitapi/generator.rs index f23c09eebf..f2bf08c099 100644 --- a/kbs/src/plugins/implementations/splitapi/generator.rs +++ b/kbs/src/plugins/implementations/splitapi/generator.rs @@ -1,27 +1,29 @@ -// Copyright (c) 2024 by IBM Corporation +// Copyright (c) 2025 by IBM Corporation // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 +use anyhow::{Context, Result}; +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::hash::MessageDigest; +use openssl::pkey::PKey; use openssl::rsa::Rsa; -use openssl::x509::{X509NameBuilder, X509Name, X509, X509ReqBuilder, X509Req}; +use openssl::x509::extension::AuthorityKeyIdentifier; use openssl::x509::extension::BasicConstraints; use openssl::x509::extension::KeyUsage; use openssl::x509::extension::SubjectKeyIdentifier; -use openssl::x509::extension::AuthorityKeyIdentifier; -use openssl::pkey::PKey; use openssl::x509::X509Builder; -use openssl::hash::MessageDigest; +use openssl::x509::{X509Name, X509NameBuilder, X509Req, X509ReqBuilder, X509}; +use serde::Deserialize; +use std::fs; use std::fs::File; -use std::io::Write; use std::io::Read; -use openssl::asn1::Asn1Time; -use openssl::bn::BigNum; +use std::io::Write; +use std::path::Path; use std::path::PathBuf; -use std::error::Error; -use anyhow::Result; - -use super::backend::SandboxParams; +use tempfile::TempDir; +use super::manager::Credentials; pub const CA_KEY_FILENAME: &str = "ca.key"; pub const CA_CRT_FILENAME: &str = "ca.pem"; @@ -32,21 +34,66 @@ pub const SERVER_KEY_FILENAME: &str = "server.key"; pub const SERVER_CSR_FILENAME: &str = "server.csr"; pub const SERVER_CRT_FILENAME: &str = "server.pem"; -const CREDENTIAL_KEY_SIZE: u32 = 2048; +const KEY_SIZE: u32 = 2048; +/// Default certificate details if not configured +pub const DEFAULT_COUNTRY: &str = "AA"; +pub const DEFAULT_STATE: &str = "Default State"; +pub const DEFAULT_LOCALITY: &str = "Default City"; +pub const DEFAULT_ORGANIZATION: &str = "Default Organization"; +pub const DEFAULT_ORG_UNIT: &str = "Default Unit"; +pub const DEFAULT_CA_COMMON_NAME: &str = "grpc-tls CA"; +pub const DEFAULT_SERVER_COMMON_NAME: &str = "server"; +pub const DEFAULT_CLIENT_COMMON_NAME: &str = "client"; +pub const DEFAULT_CA_VALIDITY_DAYS: u32 = 3650; +pub const DEFAULT_SERVER_VALIDITY_DAYS: u32 = 180; +pub const DEFAULT_CLIENT_VALIDITY_DAYS: u32 = 180; -#[derive(Debug, serde::Serialize)] -pub struct ServerCredential { - pub key: Vec, - pub crt: Vec, - pub ca_crt: Vec, +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct CertificateDetails { + pub country: String, + pub state: String, + pub locality: String, + pub organization: String, + pub org_unit: String, + pub ca: Certificate, + pub server: Certificate, + pub client: Certificate, } +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct Certificate { + pub common_name: String, + pub validity_days: u32, +} + +impl Default for CertificateDetails { + fn default() -> Self { + CertificateDetails { + country: DEFAULT_COUNTRY.to_string(), + state: DEFAULT_STATE.to_string(), + locality: DEFAULT_LOCALITY.to_string(), + organization: DEFAULT_ORGANIZATION.to_string(), + org_unit: DEFAULT_ORG_UNIT.to_string(), + ca: Certificate { + common_name: DEFAULT_CA_COMMON_NAME.to_string(), + validity_days: DEFAULT_CA_VALIDITY_DAYS, + }, + server: Certificate { + common_name: DEFAULT_SERVER_COMMON_NAME.to_string(), + validity_days: DEFAULT_SERVER_VALIDITY_DAYS, + }, + client: Certificate { + common_name: DEFAULT_CLIENT_COMMON_NAME.to_string(), + validity_days: DEFAULT_CLIENT_VALIDITY_DAYS, + }, + } + } +} -/// Credentials (keys and certs for ca, server, and client) stored -/// in plugin_dir/sandbox-specific-directory #[derive(Debug)] -pub struct CredentialBundle { +pub struct CredentialGenerator { key_size: u32, ca_key: PathBuf, ca_crt: PathBuf, @@ -55,127 +102,126 @@ pub struct CredentialBundle { client_crt: PathBuf, server_key: PathBuf, server_csr: PathBuf, - server_crt: PathBuf + server_crt: PathBuf, } -impl CredentialBundle { - pub fn new(sandbox_dir: &PathBuf) -> Result { - let ca_key: PathBuf = sandbox_dir.as_path().join(CA_KEY_FILENAME); - let ca_crt: PathBuf = sandbox_dir.as_path().join(CA_CRT_FILENAME); - - let client_key: PathBuf = sandbox_dir.as_path().join(CLIENT_KEY_FILENAME); - let client_csr: PathBuf = sandbox_dir.as_path().join(CLIENT_CSR_FILENAME); - let client_crt: PathBuf = sandbox_dir.as_path().join(CLIENT_CRT_FILENAME); - - let server_key: PathBuf = sandbox_dir.as_path().join(SERVER_KEY_FILENAME); - let server_csr: PathBuf = sandbox_dir.as_path().join(SERVER_CSR_FILENAME); - let server_crt: PathBuf = sandbox_dir.as_path().join(SERVER_CRT_FILENAME); - +impl CredentialGenerator { + pub fn new(cred_dir: &TempDir) -> Result { Ok(Self { - key_size: CREDENTIAL_KEY_SIZE, - ca_key, - ca_crt, - client_key, - client_csr, - client_crt, - server_key, - server_csr, - server_crt + key_size: KEY_SIZE, + ca_key: cred_dir.path().to_owned().join(CA_KEY_FILENAME), + ca_crt: cred_dir.path().to_owned().join(CA_CRT_FILENAME), + client_key: cred_dir.path().to_owned().join(CLIENT_KEY_FILENAME), + client_csr: cred_dir.path().to_owned().join(CLIENT_CSR_FILENAME), + client_crt: cred_dir.path().to_owned().join(CLIENT_CRT_FILENAME), + server_key: cred_dir.path().to_owned().join(SERVER_KEY_FILENAME), + server_csr: cred_dir.path().to_owned().join(SERVER_CSR_FILENAME), + server_crt: cred_dir.path().to_owned().join(SERVER_CRT_FILENAME), }) } - pub fn server_key(&self) -> &PathBuf { - &self.server_key - } - - pub fn server_crt(&self) -> &PathBuf { - &self.server_crt - } - - pub fn ca_crt(&self) -> &PathBuf { - &self.ca_crt - } /// Run several steps for generate all the keys and certificates - pub fn generate( - &self, - params: &SandboxParams, - ) -> Result<&Self> { - //let mut args: Vec = Vec::from(params); - log::info!("Params {:?}", params); - - match self.generate_private_key(&self.ca_key, self.key_size) { - Ok(_) => println!("CA key generation succeeded and saved to {}.", self.ca_key.display()), - Err(e) => eprintln!("CA key generation failed: {}", e), - } - - match self.generate_ca_cert(&self.ca_crt, &self.ca_key) { - Ok(_) => println!("CA self-signed certificate generated and saved to {}.", self.ca_crt.display()), - Err(e) => eprintln!("CA self-signed certificate generation failed: {}", e), - } - - match self.generate_private_key(&self.server_key, self.key_size) { - Ok(_) => println!("Server key generation succeeded and saved to {}.", self.server_key.display()), - Err(e) => eprintln!("Server key generation failed: {}", e), - } - - let server_common_name = "server"; - match self.generate_csr(&self.server_csr, &self.server_key, server_common_name) { - Ok(_) => println!("Server csr generation succeeded and saved to {}.", self.server_csr.display()), - Err(e) => eprintln!("Server csr generation failed: {}", e), - } + pub fn generate(&self, cert_details: &CertificateDetails) -> Result { + // Create CA key, and self-signed certificate (valid for 10 years) + self.generate_private_key(self.ca_key.as_path(), self.key_size)?; + let ca_x509_name = self.build_x509_name( + &cert_details.ca.common_name, + &cert_details.country, + &cert_details.state, + &cert_details.locality, + &cert_details.organization, + &cert_details.org_unit, + )?; + self.generate_ca_cert( + self.ca_crt.as_path(), + self.ca_key.as_path(), + &ca_x509_name, + cert_details.ca.validity_days, + )?; - match self.generate_cert(&self.server_crt, &self.server_csr, &self.ca_crt, &self.ca_key) { - Ok(_) => println!("Server cert generation succeeded and saved to {}.", self.server_crt.display()), - Err(e) => eprintln!("Server cert generation failed: {}", e), - } + // Create server key, csr, and certificate + self.generate_private_key(self.server_key.as_path(), self.key_size)?; + let server_x509_name = self.build_x509_name( + &cert_details.server.common_name, + &cert_details.country, + &cert_details.state, + &cert_details.locality, + &cert_details.organization, + &cert_details.org_unit, + )?; + self.generate_csr( + self.server_csr.as_path(), + self.server_key.as_path(), + &server_x509_name, + )?; + self.generate_cert( + self.server_crt.as_path(), + self.server_csr.as_path(), + self.ca_crt.as_path(), + self.ca_key.as_path(), + cert_details.server.validity_days, + )?; - match self.generate_private_key(&self.client_key, self.key_size) { - Ok(_) => println!("Client key generation succeeded and saved to {}.", self.client_key.display()), - Err(e) => eprintln!("Client key generation failed: {}", e), - } + // Create client key, csr, and certificate + self.generate_private_key(self.client_key.as_path(), self.key_size)?; + let client_x509_name = self.build_x509_name( + &cert_details.client.common_name, + &cert_details.country, + &cert_details.state, + &cert_details.locality, + &cert_details.organization, + &cert_details.org_unit, + )?; + self.generate_csr( + self.client_csr.as_path(), + self.client_key.as_path(), + &client_x509_name, + )?; + self.generate_cert( + self.client_crt.as_path(), + self.client_csr.as_path(), + self.ca_crt.as_path(), + self.ca_key.as_path(), + cert_details.client.validity_days, + )?; - let client_common_name = "client"; - match self.generate_csr(&self.client_csr, &self.client_key, client_common_name) { - Ok(_) => println!("Client CSR generation succeeded and saved to {}.", self.client_csr.display()), - Err(e) => eprintln!("Client CSR generation failed: {}", e), - } + // Read the generated credentials + let read_cred = + |path: &Path| fs::read(path).with_context(|| format!("read {}", path.display())); - match self.generate_cert(&self.client_crt, &self.client_csr, &self.ca_crt, &self.ca_key) { - Ok(_) => println!("Client cert generation succeeded and saved to {}.", self.client_crt.display()), - Err(e) => eprintln!("Client cert generation failed: {}", e), - } + let credentials = Credentials { + ca_crt: read_cred(self.ca_crt.as_path())?, + client_key: read_cred(self.client_key.as_path())?, + client_crt: read_cred(self.client_crt.as_path())?, + server_key: read_cred(self.server_key.as_path())?, + server_crt: read_cred(self.server_crt.as_path())?, + }; - Ok(self) + Ok(credentials) } - fn generate_private_key( - &self, - ca_key_path: &PathBuf, - key_size: u32 - ) -> Result<(), Box> { + fn generate_private_key(&self, ca_key_path: &Path, key_size: u32) -> Result<()> { // Generate RSA key let rsa = Rsa::generate(key_size).expect("Failed to generate RSA key"); let pkey = PKey::from_rsa(rsa).expect("Failed to create PKey from RSA"); - + // Write the private key to a file let private_key_pem = pkey.private_key_to_pem_pkcs8()?; - let mut file = File::create(ca_key_path.as_path())?; + let mut file = File::create(ca_key_path)?; file.write_all(&private_key_pem)?; - + Ok(()) } fn build_x509_name( - &self, - common_name: &str - ) -> Result> { - // Define certificate details - let country = "AA"; - let state = "Default State"; - let locality = "Default City"; - let organization = "Default Organization"; - let org_unit = "Default Unit"; - + &self, + common_name: &str, + country: &str, + state: &str, + locality: &str, + organization: &str, + org_unit: &str, + ) -> Result { // Build X.509 name let mut name_builder = X509NameBuilder::new()?; name_builder.append_entry_by_text("C", country)?; @@ -185,115 +231,112 @@ impl CredentialBundle { name_builder.append_entry_by_text("OU", org_unit)?; name_builder.append_entry_by_text("CN", common_name)?; let name = name_builder.build(); - + Ok(name) } - + fn generate_ca_cert( - &self, - crt_path: &PathBuf, - ca_key_path: &PathBuf - ) -> Result<(), Box> { + &self, + crt_path: &Path, + ca_key_path: &Path, + name: &X509Name, + validity_days: u32, + ) -> Result<()> { // Read the private key from file - let mut file = File::open(ca_key_path.as_path())?; + let mut file = File::open(ca_key_path)?; let mut key_pem = Vec::new(); file.read_to_end(&mut key_pem)?; let rsa = Rsa::private_key_from_pem(&key_pem)?; let pkey = PKey::from_rsa(rsa)?; - - // Build X.509 name - let common_name = "grpc-tls CA"; - let name = self.build_x509_name(common_name)?; - + // Build the X.509 certificate let mut x509_builder = X509Builder::new()?; - x509_builder.set_subject_name(&name)?; - x509_builder.set_issuer_name(&name)?; + x509_builder.set_subject_name(name)?; + x509_builder.set_issuer_name(name)?; x509_builder.set_pubkey(&pkey)?; - + // Set certificate validity period - x509_builder.set_not_before( - &Asn1Time::days_from_now(0).expect("Failed to set not before") - ).expect("Failed to set not before"); - x509_builder.set_not_after( - &Asn1Time::days_from_now(3650).expect("Failed to set not after") - ).expect("Failed to set not after"); - + x509_builder + .set_not_before(&Asn1Time::days_from_now(0).expect("Failed to set not before")) + .expect("Failed to set not before"); + x509_builder + .set_not_after( + &Asn1Time::days_from_now(validity_days).expect("Failed to set not after"), + ) + .expect("Failed to set not after"); + // Sign the certificate x509_builder.sign(&pkey, MessageDigest::sha256())?; let x509 = x509_builder.build(); - + // Write the certificate to a file let crt_pem = x509.to_pem()?; - let mut crt_file = File::create(crt_path.as_path())?; + let mut crt_file = File::create(crt_path)?; crt_file.write_all(&crt_pem)?; - + Ok(()) } - + fn generate_csr( - &self, - csr_path: &PathBuf, - private_key_path: &PathBuf, - common_name: &str - ) -> Result<(), Box> { - + &self, + csr_path: &Path, + private_key_path: &Path, + name: &X509Name, + ) -> Result<()> { // Read the private key from file - let mut file = File::open(private_key_path.as_path())?; + let mut file = File::open(private_key_path)?; let mut key_pem = Vec::new(); file.read_to_end(&mut key_pem)?; let rsa = Rsa::private_key_from_pem(&key_pem)?; let pkey = PKey::from_rsa(rsa)?; - - // Build X.509 name - let name = self.build_x509_name(common_name)?; - + // Create a new X.509 certificate signing request (CSR) let mut csr_builder = X509ReqBuilder::new()?; - csr_builder.set_subject_name(&name)?; + csr_builder.set_subject_name(name)?; csr_builder.set_pubkey(&pkey)?; csr_builder.sign(&pkey, MessageDigest::sha256())?; - + let csr = csr_builder.build(); - + // Write CSR to a file - let mut csr_file = File::create(csr_path.as_path())?; + let mut csr_file = File::create(csr_path)?; csr_file.write_all(&csr.to_pem()?)?; - + Ok(()) } - + fn generate_cert( - &self, - crt_path: &PathBuf, - csr_path: &PathBuf, - ca_crt_path: &PathBuf, - ca_key_path: &PathBuf - ) -> Result<(), Box> { + &self, + crt_path: &Path, + csr_path: &Path, + ca_crt_path: &Path, + ca_key_path: &Path, + validity_days: u32, + ) -> Result<()> { // Step 1: Read the CSR - let mut csr_file = File::open(csr_path.as_path())?; + let mut csr_file = File::open(csr_path)?; let mut csr_data = vec![]; csr_file.read_to_end(&mut csr_data)?; let csr = X509Req::from_pem(&csr_data)?; - + // Step 2: Read the CA PEM - let mut ca_file = File::open(ca_crt_path.as_path())?; + let mut ca_file = File::open(ca_crt_path)?; let mut ca_data = vec![]; ca_file.read_to_end(&mut ca_data)?; let ca_cert = X509::from_pem(&ca_data)?; - + // Step 3: Read the CA Key - let mut ca_key_file = File::open(ca_key_path.as_path())?; + let mut ca_key_file = File::open(ca_key_path)?; let mut ca_key_data = vec![]; ca_key_file.read_to_end(&mut ca_key_data)?; let ca_key = PKey::private_key_from_pem(&ca_key_data)?; - + // Step 5: Create the server certificate let mut builder = X509Builder::new()?; - + // Set the version of the certificate builder.set_version(2)?; - + // Set the serial number let serial_number = { let mut serial = BigNum::new()?; @@ -301,50 +344,50 @@ impl CredentialBundle { serial.to_asn1_integer()? }; builder.set_serial_number(&serial_number)?; - + // Set the subject name from the CSR builder.set_subject_name(csr.subject_name())?; //TODO: add sandbox IP in the subject - + // Set the issuer name from the CA certificate builder.set_issuer_name(ca_cert.subject_name())?; - - // Set the public key from the CSR - let public_key = csr.public_key()?; + + // Set the public key from the CSR + let public_key = csr.public_key()?; builder.set_pubkey(&public_key)?; - - // Set the certificate validity period - let not_before = openssl::asn1::Asn1Time::days_from_now(0)?; - let not_after = openssl::asn1::Asn1Time::days_from_now(3650)?; - builder.set_not_before(¬_before)?; + + // Set the certificate validity period + let not_before = openssl::asn1::Asn1Time::days_from_now(0)?; + let not_after = openssl::asn1::Asn1Time::days_from_now(validity_days)?; + builder.set_not_before(¬_before)?; builder.set_not_after(¬_after)?; - - // Add extensions from the certificate extensions file - builder.append_extension(BasicConstraints::new().critical().build()?)?; + + // Add extensions from the certificate extensions file + builder.append_extension(BasicConstraints::new().critical().build()?)?; builder.append_extension( KeyUsage::new() - .digital_signature() - .key_encipherment().build()? - )?; + .digital_signature() + .key_encipherment() + .build()?, + )?; builder.append_extension( - SubjectKeyIdentifier::new() - .build(&builder.x509v3_context(None, None))? - )?; + SubjectKeyIdentifier::new().build(&builder.x509v3_context(None, None))?, + )?; builder.append_extension( AuthorityKeyIdentifier::new() - .keyid(false) - .issuer(false) - .build(&builder.x509v3_context(Some(&ca_cert), None))? + .keyid(false) + .issuer(false) + .build(&builder.x509v3_context(Some(&ca_cert), None))?, )?; - - // Sign the certificate with the CA key - builder.sign(&ca_key, MessageDigest::sha256())?; - - // Write the server certificate to a file - let server_crt = builder.build().to_pem()?; - let mut crt_file = File::create(crt_path.as_path())?; + + // Sign the certificate with the CA key + builder.sign(&ca_key, MessageDigest::sha256())?; + + // Write the server certificate to a file + let server_crt = builder.build().to_pem()?; + let mut crt_file = File::create(crt_path)?; crt_file.write_all(&server_crt)?; - + Ok(()) } -} \ No newline at end of file +} diff --git a/kbs/src/plugins/implementations/splitapi/manager.rs b/kbs/src/plugins/implementations/splitapi/manager.rs index e4c3fbc5cf..03f93f3a63 100644 --- a/kbs/src/plugins/implementations/splitapi/manager.rs +++ b/kbs/src/plugins/implementations/splitapi/manager.rs @@ -1,163 +1,148 @@ -// Copyright (c) 2024 by IBM Corporation +// Copyright (c) 2025 by IBM Corporation // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use anyhow::{anyhow, Context, Result}; -use std::{sync::Arc}; -use serde::Deserialize; +use anyhow::Result; +use serde::{Deserialize, Serialize}; use std::{ + collections::HashMap, fs, - path::{Path, PathBuf}, + path::PathBuf, + sync::atomic::{AtomicBool, Ordering}, + sync::Arc, }; -use std::sync::Mutex; -use lazy_static::lazy_static; - -use super::backend::{SplitAPIBackend, SandboxParams}; -use super::mapper::{SandboxDirectoryMapper, SandboxDirectoryInfo}; -use super::generator::{CredentialBundle, ServerCredential}; - - -pub const DEFAULT_PLUGIN_DIR: &str = "/opt/confidential-containers/kbs/plugin/splitapi"; -pub const SANDBOX_DIRECTORY_MAPPING_FILENAME: &str = "sandbox-credential-mapping.json"; - - -// Use lazy_static to initialize the SANDBOX_DIRECTORY_MANAGER only once -lazy_static! { - static ref SANDBOX_DIRECTORY_MAPPER: Arc>> = Arc::new(Mutex::new(None)); -} - -// Initialize the singleton with the provided file path -fn init_sandbox_directory_mapper(file_path: PathBuf) -> std::io::Result<()> { - let mut mapper = SANDBOX_DIRECTORY_MAPPER.lock().unwrap(); - - // Attempt to load the DirectoryManager from the file - match SandboxDirectoryMapper::load_from_file(file_path) { - Ok(loaded_mapper) => { - *mapper = Some(loaded_mapper); - } - Err(_e) => { - // Initialize a new manager - *mapper = Some(SandboxDirectoryMapper::new()); - - // TODO: check specific errors (file not found or something else) - // and handle those specific errors - // bail if there's relevant condition - } - } - - Ok(()) +use tempfile::tempdir_in; +use tokio::sync::RwLock; + +use super::backend::{SandboxParams, SplitAPIBackend}; +use super::generator::{CertificateDetails, CredentialGenerator}; + +/// Credentials (keys and certs for CA, server, and client) +/// ncessary for the SplitAPI work +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Credentials { + pub ca_crt: Vec, + pub client_key: Vec, + pub client_crt: Vec, + pub server_key: Vec, + pub server_crt: Vec, } -// Get a reference to the singleton -fn get_sandbox_directory_mapper() -> Arc>> { - Arc::clone(&SANDBOX_DIRECTORY_MAPPER) +/// Credentials necessary for SplitAPI proxy server +#[derive(Debug, serde::Serialize)] +pub struct ServerCredentials { + pub key: Vec, + pub crt: Vec, + pub ca_crt: Vec, } - -#[derive(Debug, Deserialize, Clone, PartialEq)] -pub struct SplitAPIRepoDesc { - #[serde(default)] - pub plugin_dir: String, +/// Manages the credentials generation, handling requests +/// from backend, and credentials persistence storage +pub struct CertManager { + pub plugin_dir: PathBuf, + pub certificate_details: CertificateDetails, + pub credblob_file: PathBuf, //String, + pub state: Arc>>, + credential_loaded_from_file: AtomicBool, } -impl Default for SplitAPIRepoDesc { - fn default() -> Self { - Self { - plugin_dir: DEFAULT_PLUGIN_DIR.into(), +impl CertManager { + pub fn new( + plugin_dir: PathBuf, + blob_file: String, + cert_details: &CertificateDetails, + ) -> anyhow::Result { + if !plugin_dir.exists() { + fs::create_dir_all(&plugin_dir)?; + log::info!("plugin dir created = {}", plugin_dir.display()); } - } -} - -pub struct CertManager { - pub plugin_dir: String, - pub mapping_filename: String, - mapper: Arc>>, -} + let cblob_file = plugin_dir.as_path().join(blob_file); + // Initialize the credential manager + Ok(Self { + plugin_dir, + certificate_details: cert_details.clone(), + credblob_file: cblob_file, + state: Arc::new(RwLock::new(HashMap::new())), + credential_loaded_from_file: AtomicBool::new(false), + }) + } -impl CertManager { - pub fn new(repo_desc: &SplitAPIRepoDesc) -> anyhow::Result { - // Create splitapi_res work dir. - if !Path::new(&repo_desc.plugin_dir).exists() { - fs::create_dir_all(&repo_desc.plugin_dir)?; + async fn load_credentials(&self, key: &str) -> Option { + // Check if the credential is not loaded. If not, load them + if !self.credential_loaded_from_file.load(Ordering::SeqCst) { + if let Err(e) = self.load_from_file(&self.credblob_file).await { + log::warn!("Failed to load credentials from file: {}", e); + return None; + } - log::info!("Splitapi plugin directory created = {}", repo_desc.plugin_dir); + // Update the flag, this is a one-time load until kbs restarts + self.credential_loaded_from_file + .store(true, Ordering::SeqCst); } - // Initialize directory manager with the content from a file - let mapping_file: PathBuf = PathBuf::from(&repo_desc.plugin_dir) - .as_path() - .join(SANDBOX_DIRECTORY_MAPPING_FILENAME - ); - init_sandbox_directory_mapper(mapping_file.clone())?; - log::info!("Directory manager loaded the data from file: {}", mapping_file.display()); + // Return the item from hashmap + let state = self.state.read().await; + state.get(key).cloned() + } - // Initialize the manager - Ok(Self { - plugin_dir: repo_desc.plugin_dir.clone(), - mapping_filename: SANDBOX_DIRECTORY_MAPPING_FILENAME.into(), - mapper: get_sandbox_directory_mapper(), - }) + async fn load_from_file(&self, path: &PathBuf) -> Result<()> { + let data = tokio::fs::read_to_string(&path).await?; + let deserialized: HashMap = serde_json::from_str(&data)?; + let mut state = self.state.write().await; + *state = deserialized; + Ok(()) + } + + async fn save_to_file(&self, path: &PathBuf) -> Result<()> { + let state = self.state.read().await; + let serialized = serde_json::to_string(&*state)?; + tokio::fs::write(path, serialized).await?; + Ok(()) } } #[async_trait::async_trait] impl SplitAPIBackend for CertManager { async fn get_server_credential(&self, params: &SandboxParams) -> Result> { - // Try locking the sandbox directory mapper - let mut mapper_guard = self.mapper.lock().map_err(|e| { - anyhow!("Failed to lock sandbox directory mapper: {}", e) - })?; - - if let Some(mapper) = mapper_guard.as_mut() { - let sandbox_dir_info: SandboxDirectoryInfo; - - if let Some(existing_dir) = mapper.get_directory(¶ms.name) { - - log::info!("Found existing directory: {:?}", existing_dir.sandbox_dir()); - sandbox_dir_info = existing_dir.clone(); - - //TODO: check if the credentails are already in there - // send the existing credentials if they are not expired - } else { - let new_dir_info = mapper.create_directory( - Path::new(&self.plugin_dir), - ¶ms - )?; - log::info!("New directory created: {:?}", new_dir_info); - - let mapping_file = PathBuf::from(&self.plugin_dir) - .as_path() - .join(&self.mapping_filename); - - mapper.write_to_file( - &new_dir_info, - &mapping_file - )?; - - sandbox_dir_info = new_dir_info; - } - - // Generate the credentials (keys and certs for ca, server, and client) - let cred_bundle = CredentialBundle::new(sandbox_dir_info.sandbox_dir())?; - cred_bundle.generate(params)?; - - // Return the server specific credentials - let resource = ServerCredential { - key: fs::read(cred_bundle.server_key().as_path()) - .with_context(|| format!("read {}", cred_bundle.server_key().display()))?, - crt: fs::read(cred_bundle.server_crt().as_path()) - .with_context(|| format!("read {}", cred_bundle.server_crt().display()))?, - ca_crt: fs::read(cred_bundle.ca_crt().as_path()) - .with_context(|| format!("read {}", cred_bundle.ca_crt().display()))?, + // Return the server credential if the credential presents in the hashmap + let key = format!("{}_{}_{}", ¶ms.name, ¶ms.ip, ¶ms.id); + if let Some(credentials) = self.load_credentials(&key).await { + log::info!("Returning already existed credentials!"); + + let resource = ServerCredentials { + key: credentials.server_key, + crt: credentials.server_crt, + ca_crt: credentials.ca_crt, }; - - Ok(serde_json::to_vec(&resource)?) - } else { - // Handle the case where the manager is None - Err(anyhow!("Directory manager is uninitialized")) + return Ok(serde_json::to_vec(&resource)?); + }; + + // Generate the credentials (keys and certs for ca, server, and client) + let credential_dir = tempdir_in(self.plugin_dir.as_path())?; + let generator = CredentialGenerator::new(&credential_dir)?; + let credentials = generator.generate(&self.certificate_details)?; + + log::info!("Credentials are generated!"); + + // Aquire the write lock and write the credential into the hashmap + { + let mut state = self.state.write().await; + state.insert(key, credentials.clone()); } + + // Write the hashmap to file for a persistence copy + self.save_to_file(&self.credblob_file).await?; + + // Return the server credentials to respond the request + let resource = ServerCredentials { + key: credentials.server_key.clone(), + crt: credentials.server_crt.clone(), + ca_crt: credentials.ca_crt.clone(), + }; + + Ok(serde_json::to_vec(&resource)?) } -} \ No newline at end of file +} diff --git a/kbs/src/plugins/implementations/splitapi/mapper.rs b/kbs/src/plugins/implementations/splitapi/mapper.rs deleted file mode 100644 index f3b5272700..0000000000 --- a/kbs/src/plugins/implementations/splitapi/mapper.rs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) 2024 by IBM Corporation -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use std::fs::File; -use std::io::Write; -use std::path::{Path, PathBuf}; -use anyhow::{Context, Result}; -use std::io; -use std::fs; -use std::collections::HashMap; -use std::fs::OpenOptions; -use std::io::{BufRead, BufReader}; - -use super::backend::SandboxParams; - - -/// It has the fields to store the mapping between a sandbox name or id -/// to a unique directory created by the directory manager -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct SandboxDirectoryInfo { - id: String, - ip: String, - name: String, - sandbox_dir: PathBuf, -} - -impl SandboxDirectoryInfo { - pub fn sandbox_dir(&self) -> &PathBuf { - &self.sandbox_dir - } -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct SandboxDirectoryMapper { - // Maps sandbox name to SandboxDirectoryInfo - sandbox_directory_mapper: HashMap, -} - - -/// Responsible for generating, storing, or loading unique directory -/// names for each sandbox. That means it creates a unique directory -/// for a sandbox (if the directory does not already exist), and stores -/// the mapping between the sandbox name (or id) to a file -impl SandboxDirectoryMapper { - pub fn new() -> Self { - SandboxDirectoryMapper { - sandbox_directory_mapper: HashMap::new(), - } - } - - // Generate a unique directory name from the fields - fn generate_unique_dirname(id: &str, ip: &str, name: &str) -> String { - format!("{}_{}_{}", name, ip, id) - } - - // Create a directory and store it in the HashMap - pub fn create_directory( - &mut self, - plugin_dir: &Path, - params: &SandboxParams - ) -> Result { - - let directory_name = SandboxDirectoryMapper::generate_unique_dirname( - ¶ms.id, - ¶ms.ip, - ¶ms.name - ); - - let directory_path: PathBuf = PathBuf::from(plugin_dir).as_path().join(&directory_name); - - // Create the directory - fs::create_dir_all(&directory_path.clone()) - .with_context(|| format!("Create {} dir", directory_path.display()))?; - - log::info!("Directory {} created", directory_name); - - // Store directory info in the HashMap - let dir_info = SandboxDirectoryInfo { - id: params.id.clone(), - ip: params.ip.clone(), - name: params.name.clone(), - sandbox_dir: directory_path.clone(), - }; - - self.sandbox_directory_mapper.insert(params.name.clone(), dir_info.clone()); - - Ok(dir_info) - } - - // Retrieve the directory info by name - pub fn get_directory(&self, name: &str) -> Option<&SandboxDirectoryInfo> { - self.sandbox_directory_mapper.get(name) - } - - // Function to write SandboxDirectoryInfo to a JSON file - pub fn write_to_file( - &self, - dir_info: &SandboxDirectoryInfo, - file_path: &PathBuf - ) -> io::Result<()> { - - let file = OpenOptions::new().append(true).create(true).open(file_path)?; - let mut writer = std::io::BufWriter::new(file); - - // Serialize the SandboxDirectoryInfo entry and append it to the file with a newline delimiter - serde_json::to_writer(&mut writer, &dir_info)?; - writer.write_all(b"\n")?; // Add a newline to separate entries - - Ok(()) - } - - // Load the directory data from a JSON file - pub fn load_from_file(file_path: PathBuf) -> Result { - - log::info!("Loading directory info: {}", file_path.display()); - - let file = File::open(file_path)?; - let reader = BufReader::new(file); - - // Create a new SandboxDirectoryMapper and populate its HashMap - let mut mapper = SandboxDirectoryMapper::new(); - - for line in reader.lines() { - let line = line?; - let entry: SandboxDirectoryInfo = serde_json::from_str(&line)?; - log::info!("{:?}", entry); - mapper.sandbox_directory_mapper.insert(entry.name.clone(), entry); - } - - Ok(mapper) - } -} \ No newline at end of file diff --git a/kbs/src/plugins/implementations/splitapi/mod.rs b/kbs/src/plugins/implementations/splitapi/mod.rs index 223185132b..3084be8424 100644 --- a/kbs/src/plugins/implementations/splitapi/mod.rs +++ b/kbs/src/plugins/implementations/splitapi/mod.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 by IBM Corporation +// Copyright (c) 2025 by IBM Corporation // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 @@ -7,19 +7,17 @@ //! server and establish a secure tunnel between tenant and the API proxy //! server. -pub mod manager; -pub mod mapper; pub mod generator; +pub mod manager; use actix_web::http::Method; -use anyhow::{anyhow, Error, bail, Result}; +use anyhow::{anyhow, bail, Context, Result}; pub mod backend; pub use backend::*; use super::super::plugin_manager::ClientPlugin; - #[async_trait::async_trait] impl ClientPlugin for SplitAPI { async fn handle( @@ -29,21 +27,21 @@ impl ClientPlugin for SplitAPI { path: &str, method: &Method, ) -> Result> { + let sub_path = path + .strip_prefix('/') + .context("accessed path is illegal, should start with `/`")?; if method.as_str() != "GET" { - bail!("Illegal HTTP method**. Only supports `GET`") + bail!("Illegal HTTP method. Only GET is supported"); } - match path { + match sub_path { "credential" => { - let params: SandboxParams = - serde_qs::from_str(query).map_err(|e| { - anyhow!("Failed to parse query string: {}", e) - })?; + let params = SandboxParams::try_from(query)?; let credential = self.backend.get_server_credential(¶ms).await?; Ok(credential) } - _ => Err(Error::msg("Illegal format of the request")) + _ => Err(anyhow!("{} not supported", sub_path))?, } } @@ -67,68 +65,58 @@ impl ClientPlugin for SplitAPI { _path: &str, _method: &Method, ) -> Result { - Ok(false) + Ok(true) } } - #[cfg(test)] mod tests { use super::*; - use tokio; + use std::path::PathBuf; use std::sync::Arc; - use std::{ - fs, - path::{PathBuf}, - }; - use anyhow::Context; - - use super::generator::{CA_CRT_FILENAME, SERVER_KEY_FILENAME, SERVER_CRT_FILENAME}; - use super::generator::ServerCredential; + use tokio; + use super::manager::{Credentials, ServerCredentials}; #[tokio::test] async fn test_handle() { - // Arrange: create an instance of `SplitAPI` - let desc = manager::SplitAPIRepoDesc::default(); - let backend = manager::CertManager::new(&desc); + let plugin_dir = "/opt/confidential-containers/kbs/plugin/splitapi"; + let config = SplitAPIConfig::default(); + let backend = manager::CertManager::new( + PathBuf::from(&plugin_dir), + config.credential_blob_filename, + &config.certificate_details, + ); let backend = Arc::new(backend.expect("Failed to initialize backend")); - let split_api = SplitAPI { backend: backend }; - + let split_api = SplitAPI { + backend: backend.clone(), + }; + // Define sample inputs let body: &[u8] = b""; let query = "id=3367348&ip=60.11.12.43&name=pod7"; - let path = "credential"; + let path = "/credential"; let method = &Method::GET; // Act: call the handle method let result = split_api.handle(body, query, path, method).await; - println!("plugin dir = {}", desc.plugin_dir); - // Assert: check the result match result { Ok(response) => { - // Expected results - let sandbox_dir = PathBuf::from(&desc.plugin_dir) - .as_path() - .join("pod7_60.11.12.43_3367348"); - - let server_key = sandbox_dir.as_path().join(SERVER_KEY_FILENAME); - let server_crt = sandbox_dir.as_path().join(SERVER_CRT_FILENAME); - let ca_crt = sandbox_dir.as_path().join(CA_CRT_FILENAME); - - let resource = ServerCredential { - key: fs::read(server_key.as_path()) - .with_context(|| format!("read {}", server_key.display())) - .expect("failed to read server key"), - crt: fs::read(server_crt.as_path()) - .with_context(|| format!("read {}", server_crt.display())) - .expect("failed to read server crt"), - ca_crt: fs::read(ca_crt.as_path()) - .with_context(|| format!("read {}", ca_crt.display())) - .expect("failed to read ca crt"), + let key = String::from("pod7_60.11.12.43_3367348"); + + let state = backend.state.read().await; + let credentials: Credentials = state + .get(&key) + .cloned() + .expect("Credentisl not found in hashmap"); + + let resource = ServerCredentials { + key: credentials.server_key, + crt: credentials.server_crt, + ca_crt: credentials.ca_crt, }; let expected_response = serde_json::to_vec(&resource).unwrap(); @@ -137,4 +125,4 @@ mod tests { Err(e) => panic!("Expected Ok, got Err: {:?}", e), } } -} \ No newline at end of file +} diff --git a/kbs/src/plugins/plugin_manager.rs b/kbs/src/plugins/plugin_manager.rs index 6702743a00..f841b16505 100644 --- a/kbs/src/plugins/plugin_manager.rs +++ b/kbs/src/plugins/plugin_manager.rs @@ -12,6 +12,8 @@ use super::{sample, RepositoryConfig, ResourceStorage}; #[cfg(feature = "nebula-ca-plugin")] use super::{NebulaCaPlugin, NebulaCaPluginConfig}; +#[cfg(feature = "splitapi-plugin")] +use super::splitapi::{SplitAPI, SplitAPIConfig}; #[cfg(feature = "pkcs11")] use super::{Pkcs11Backend, Pkcs11Config}; @@ -73,6 +75,9 @@ pub enum PluginsConfig { #[cfg(feature = "pkcs11")] #[serde(alias = "pkcs11")] Pkcs11(Pkcs11Config), + #[cfg(feature = "splitapi-plugin")] + #[serde(alias = "splitapi")] + SplitAPI(SplitAPIConfig), } impl Display for PluginsConfig { @@ -84,6 +89,8 @@ impl Display for PluginsConfig { PluginsConfig::NebulaCaPlugin(_) => f.write_str("nebula-ca"), #[cfg(feature = "pkcs11")] PluginsConfig::Pkcs11(_) => f.write_str("pkcs11"), + #[cfg(feature = "splitapi-plugin")] + PluginsConfig::SplitAPI(_) => f.write_str("splitapi"), } } } @@ -108,6 +115,11 @@ impl TryInto for PluginsConfig { let nebula_ca = NebulaCaPlugin::try_from(nebula_ca_config) .context("Initialize 'nebula-ca-plugin' failed")?; Arc::new(nebula_ca) as _ + #[cfg(feature = "splitapi-plugin")] + PluginsConfig::SplitAPI(splitapi_config) => { + let splitapi_plugin = SplitAPI::try_from(splitapi_config) + .context("Initialize 'SplitAPI' plugin failed")?; + Arc::new(splitapi_plugin) as _ } #[cfg(feature = "pkcs11")] PluginsConfig::Pkcs11(pkcs11_config) => { @@ -147,4 +159,4 @@ impl PluginManager { pub fn get(&self, name: &str) -> Option { self.plugins.get(name).cloned() } -} \ No newline at end of file +} From 3612f3a802583ede2d6567a3559f2e92ca58f17d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 5 Mar 2025 06:38:57 +0000 Subject: [PATCH 3/5] kbs: Refactor the plugin for generic use cases Updates include the following changes: - code refactor and clean up, - generic PKI-type plugin, - keys generated using Ed25519 Signed-off-by: Salman Ahmed --- Cargo.lock | 6 - kbs/Cargo.toml | 6 +- kbs/Makefile | 7 +- kbs/src/plugins/implementations/mod.rs | 8 +- .../implementations/pki_vault/credential.rs | 272 ++++++++++++ .../implementations/pki_vault/manager.rs | 267 ++++++++++++ .../plugins/implementations/pki_vault/mod.rs | 4 + .../implementations/splitapi/backend.rs | 83 ---- .../implementations/splitapi/generator.rs | 393 ------------------ .../implementations/splitapi/manager.rs | 148 ------- .../plugins/implementations/splitapi/mod.rs | 128 ------ kbs/src/plugins/plugin_manager.rs | 25 +- 12 files changed, 571 insertions(+), 776 deletions(-) create mode 100644 kbs/src/plugins/implementations/pki_vault/credential.rs create mode 100644 kbs/src/plugins/implementations/pki_vault/manager.rs create mode 100644 kbs/src/plugins/implementations/pki_vault/mod.rs delete mode 100644 kbs/src/plugins/implementations/splitapi/backend.rs delete mode 100644 kbs/src/plugins/implementations/splitapi/generator.rs delete mode 100644 kbs/src/plugins/implementations/splitapi/manager.rs delete mode 100644 kbs/src/plugins/implementations/splitapi/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8326da75b8..c54e1eb982 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3285,8 +3285,6 @@ dependencies = [ "serial_test", "sha2", "strum", - "serde_qs", - "strum", "tempfile", "thiserror 2.0.17", "time", @@ -5598,11 +5596,7 @@ checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" dependencies = [ "percent-encoding", "serde", -<<<<<<< HEAD "thiserror 1.0.69", -======= - "thiserror", ->>>>>>> cd1ac3e (kbs: Add splitapi plugin) ] [[package]] diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index 0e493baf4f..7a9ba1a14b 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -36,8 +36,10 @@ pkcs11 = ["cryptoki"] # Use Nebula Certificate Authority plugin to provide CA services to nodes # that want to join a Nebula overlay network nebula-ca-plugin = [] -# Use SplitAPI plugin to provide credentials for sandbox (pods) to initiate API proxy server -splitapi-plugin = [] + +# Use PKI Vault plugin to provide credentials for mutual TLS communication +# between a server running in the sandbox (pod) and a client (admin or owner) +pki-vault-plugin = [] # Use HashiCorp Vault KV v1 as KBS backend vault = ["vaultrs"] diff --git a/kbs/Makefile b/kbs/Makefile index b311d0c32b..36fd639327 100644 --- a/kbs/Makefile +++ b/kbs/Makefile @@ -3,6 +3,7 @@ ALIYUN ?= false NEBULA_CA_PLUGIN ?= false VAULT ?= false SPLITAPI_PLUGIN ?= false +PKI_VAULT_PLUGIN ?= false BUILD_ARCH := $(shell uname -m) ARCH ?= $(shell uname -m) @@ -55,8 +56,10 @@ endif ifeq ($(NEBULA_CA_PLUGIN), true) FEATURES += nebula-ca-plugin -ifeq ($(SPLITAPI_PLUGIN), true) - FEATURES += splitapi-plugin +endif + +ifeq ($(PKI_VAULT_PLUGIN), true) + FEATURES += pki-vault-plugin endif ifeq ($(VAULT), true) diff --git a/kbs/src/plugins/implementations/mod.rs b/kbs/src/plugins/implementations/mod.rs index d3285939eb..fbbd08d004 100644 --- a/kbs/src/plugins/implementations/mod.rs +++ b/kbs/src/plugins/implementations/mod.rs @@ -6,16 +6,16 @@ pub mod nebula_ca; #[cfg(feature = "pkcs11")] pub mod pkcs11; +#[cfg(feature = "pki-vault-plugin")] +pub mod pki_vault; pub mod resource; pub mod sample; -#[cfg(feature = "splitapi-plugin")] -pub mod splitapi; #[cfg(feature = "nebula-ca-plugin")] pub use nebula_ca::{NebulaCaPlugin, NebulaCaPluginConfig}; #[cfg(feature = "pkcs11")] pub use pkcs11::{Pkcs11Backend, Pkcs11Config}; +#[cfg(feature = "pki-vault-plugin")] +pub use pki_vault::{PKIVaultPlugin, PKIVaultPluginConfig}; pub use resource::{RepositoryConfig, ResourceStorage}; pub use sample::{Sample, SampleConfig}; -#[cfg(feature = "splitapi-plugin")] -pub use splitapi::{SplitAPI, SplitAPIConfig}; diff --git a/kbs/src/plugins/implementations/pki_vault/credential.rs b/kbs/src/plugins/implementations/pki_vault/credential.rs new file mode 100644 index 0000000000..7585158bdc --- /dev/null +++ b/kbs/src/plugins/implementations/pki_vault/credential.rs @@ -0,0 +1,272 @@ +// Copyright (c) 2025 by IBM Corporation +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Result; +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Private}; +use openssl::x509::{ + extension::{AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectKeyIdentifier}, + X509Builder, X509Name, X509NameBuilder, X509, +}; +use serde::{Deserialize, Serialize}; + +/// Default certificate details if not configured +pub const DEFAULT_COUNTRY: &str = "AA"; +pub const DEFAULT_STATE: &str = "Default State"; +pub const DEFAULT_LOCALITY: &str = "Default City"; +pub const DEFAULT_ORGANIZATION: &str = "Default Organization"; +pub const DEFAULT_ORG_UNIT: &str = "Default Unit"; +pub const DEFAULT_CA_COMMON_NAME: &str = "grpc-tls CA"; +pub const DEFAULT_SERVER_COMMON_NAME: &str = "server"; +pub const DEFAULT_CLIENT_COMMON_NAME: &str = "client"; +pub const DEFAULT_CA_VALIDITY_DAYS: u32 = 3650; +pub const DEFAULT_SERVER_VALIDITY_DAYS: u32 = 180; +pub const DEFAULT_CLIENT_VALIDITY_DAYS: u32 = 180; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct PKIVaultCertDetails { + /// Two-letter country code represents the country in which the entity resides + pub country: String, + /// State or province where the entity is located + pub state: String, + /// Locality or city where the entity is located + pub locality: String, + /// Organization or company name + pub organization: String, + /// An organizational unit within the organization + pub org_unit: String, + /// Information regarding the CA certificate + pub ca: CaCrtDetails, + /// Information regarding the server certificate + pub server: ServerCrtDetails, + /// Information regarding the client certificate + pub client: ClientCrtDetails, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct CaCrtDetails { + pub common_name: String, + pub validity_days: u32, +} + +impl Default for CaCrtDetails { + fn default() -> Self { + CaCrtDetails { + common_name: DEFAULT_CA_COMMON_NAME.to_string(), + validity_days: DEFAULT_CA_VALIDITY_DAYS, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct ServerCrtDetails { + pub common_name: String, + pub validity_days: u32, +} + +impl Default for ServerCrtDetails { + fn default() -> Self { + ServerCrtDetails { + common_name: DEFAULT_SERVER_COMMON_NAME.to_string(), + validity_days: DEFAULT_SERVER_VALIDITY_DAYS, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct ClientCrtDetails { + pub common_name: String, + pub validity_days: u32, +} + +impl Default for ClientCrtDetails { + fn default() -> Self { + ClientCrtDetails { + common_name: DEFAULT_CLIENT_COMMON_NAME.to_string(), + validity_days: DEFAULT_CLIENT_VALIDITY_DAYS, + } + } +} + +impl Default for PKIVaultCertDetails { + fn default() -> Self { + PKIVaultCertDetails { + country: DEFAULT_COUNTRY.to_string(), + state: DEFAULT_STATE.to_string(), + locality: DEFAULT_LOCALITY.to_string(), + organization: DEFAULT_ORGANIZATION.to_string(), + org_unit: DEFAULT_ORG_UNIT.to_string(), + ca: CaCrtDetails::default(), + server: ServerCrtDetails::default(), + client: ClientCrtDetails::default(), + } + } +} + +/// Credential necessary for mutual TLS communication +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Credential { + pub ca_cert: Vec, + pub server_key: Vec, + pub server_cert: Vec, + pub client_key: Vec, + pub client_cert: Vec, +} + +impl Credential { + pub fn new(cert_details: &PKIVaultCertDetails) -> Result { + // Private keys for CA, Server, and Client + let ca_private_key = PKey::generate_ed25519()?; + let server_private_key = PKey::generate_ed25519()?; + let client_private_key = PKey::generate_ed25519()?; + + // Generate CA certificate + let ca_cert = Self::generate_ca_cert(&ca_private_key, cert_details)?; + + // Generate certificate for Server + let server_cert = Self::generate_signed_cert( + &server_private_key, + &ca_cert, + &ca_private_key, + cert_details, + "server", + )?; + + // Generate certificate for Client + let client_cert = Self::generate_signed_cert( + &client_private_key, + &ca_cert, + &ca_private_key, + cert_details, + "client", + )?; + + Ok(Self { + ca_cert: ca_cert.to_pem()?, + server_key: server_private_key.private_key_to_pem_pkcs8()?, + server_cert: server_cert.to_pem()?, + client_key: client_private_key.private_key_to_pem_pkcs8()?, + client_cert: client_cert.to_pem()?, + }) + } + + fn generate_signed_cert( + private_key: &PKey, + ca_cert: &X509, + ca_private_key: &PKey, + cert_details: &PKIVaultCertDetails, + server_or_client: &str, + ) -> Result { + // Check if the certificate is for server or client + let (common_name, validity_days) = if server_or_client == "server" { + ( + &cert_details.server.common_name, + cert_details.server.validity_days, + ) + } else { + ( + &cert_details.client.common_name, + cert_details.client.validity_days, + ) + }; + + // Build the x509 name + let name = Self::build_x509_name( + common_name, + &cert_details.country, + &cert_details.state, + &cert_details.locality, + &cert_details.organization, + &cert_details.org_unit, + )?; + + let mut x509_builder = X509Builder::new()?; + x509_builder.set_version(2)?; + x509_builder.set_subject_name(&name)?; + x509_builder.set_issuer_name(ca_cert.subject_name())?; + x509_builder.set_pubkey(private_key)?; + + let serial_number = BigNum::from_u32(2)?.to_asn1_integer()?; + x509_builder.set_serial_number(&serial_number)?; + x509_builder.set_not_before(Asn1Time::days_from_now(0)?.as_ref())?; + x509_builder.set_not_after(Asn1Time::days_from_now(validity_days)?.as_ref())?; + + // Add extensions from the certificate extensions file + x509_builder.append_extension(BasicConstraints::new().critical().build()?)?; + x509_builder.append_extension( + KeyUsage::new() + .digital_signature() + .key_encipherment() + .build()?, + )?; + x509_builder.append_extension( + SubjectKeyIdentifier::new().build(&x509_builder.x509v3_context(None, None))?, + )?; + x509_builder.append_extension( + AuthorityKeyIdentifier::new() + .keyid(false) + .issuer(false) + .build(&x509_builder.x509v3_context(Some(ca_cert), None))?, + )?; + + x509_builder.sign(ca_private_key, MessageDigest::null())?; + Ok(x509_builder.build()) + } + + fn generate_ca_cert( + ca_private_key: &PKey, + cert_details: &PKIVaultCertDetails, + ) -> Result { + // Build the x509 name + let name = Self::build_x509_name( + &cert_details.ca.common_name, + &cert_details.country, + &cert_details.state, + &cert_details.locality, + &cert_details.organization, + &cert_details.org_unit, + )?; + + // Build the X.509 certificate + let mut x509_builder = X509Builder::new()?; + x509_builder.set_subject_name(&name)?; + x509_builder.set_issuer_name(&name)?; + x509_builder.set_pubkey(ca_private_key)?; + + // Set certificate validity period + x509_builder.set_not_before(Asn1Time::days_from_now(0)?.as_ref())?; + x509_builder + .set_not_after(Asn1Time::days_from_now(cert_details.ca.validity_days)?.as_ref())?; + + // Sign the certificate + x509_builder.sign(ca_private_key, MessageDigest::null())?; + Ok(x509_builder.build()) + } + + fn build_x509_name( + common_name: &str, + country: &str, + state: &str, + locality: &str, + organization: &str, + org_unit: &str, + ) -> Result { + let mut name_builder = X509NameBuilder::new()?; + name_builder.append_entry_by_text("C", country)?; + name_builder.append_entry_by_text("ST", state)?; + name_builder.append_entry_by_text("L", locality)?; + name_builder.append_entry_by_text("O", organization)?; + name_builder.append_entry_by_text("OU", org_unit)?; + name_builder.append_entry_by_text("CN", common_name)?; + let name = name_builder.build(); + + Ok(name) + } +} diff --git a/kbs/src/plugins/implementations/pki_vault/manager.rs b/kbs/src/plugins/implementations/pki_vault/manager.rs new file mode 100644 index 0000000000..d43400ccc9 --- /dev/null +++ b/kbs/src/plugins/implementations/pki_vault/manager.rs @@ -0,0 +1,267 @@ +// Copyright (c) 2025 by IBM Corporation +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use actix_web::http::Method; +use anyhow::{anyhow, bail, Context, Error, Result}; +use serde::Deserialize; +use std::sync::RwLock; +use std::{collections::HashMap, fs, path::PathBuf, sync::Arc}; + +use super::credential::{Credential, PKIVaultCertDetails}; +use crate::plugins::plugin_manager::ClientPlugin; + +const DEFAULT_PLUGIN_DIR: &str = "/opt/confidential-containers/kbs/plugin/pki_vault"; +const DEFAULT_CREDENTIALS_BLOB_FILE: &str = "certificates.json"; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct PKIVaultPluginConfig { + pub plugin_dir: String, + pub cred_filename: String, + pub pkivault_cert_details: PKIVaultCertDetails, +} + +impl Default for PKIVaultPluginConfig { + fn default() -> Self { + PKIVaultPluginConfig { + plugin_dir: DEFAULT_PLUGIN_DIR.into(), + cred_filename: DEFAULT_CREDENTIALS_BLOB_FILE.into(), + pkivault_cert_details: PKIVaultCertDetails::default(), + } + } +} + +impl TryFrom for PKIVaultPlugin { + type Error = Error; + + fn try_from(config: PKIVaultPluginConfig) -> Result { + // Create the plugin dir if it does not exist + let plugin_dir = PathBuf::from(&config.plugin_dir); + if !plugin_dir.exists() { + fs::create_dir_all(&plugin_dir)?; + log::info!("plugin dir created = {}", plugin_dir.display()); + } + + // Read the existing credentials from file + let path = PathBuf::from(&config.plugin_dir) + .as_path() + .join(config.cred_filename); + + let credential: HashMap = if path.exists() { + match fs::read_to_string(&path) { + Ok(data) => serde_json::from_str(&data).unwrap_or_else(|_| HashMap::new()), + Err(_) => { + log::warn!("Error reading the credential file."); + HashMap::new() + } + } + } else { + log::warn!("Credentail file does not exist."); + HashMap::new() + }; + + // Initializing the PKI Vault plugin with existing credential data from file + Ok(PKIVaultPlugin { + plugin_dir: PathBuf::from(&config.plugin_dir), + cert_details: config.pkivault_cert_details, + credblob_file: path, + cred_store: Arc::new(RwLock::new(credential)), + }) + } +} + +/// Parameters for the credential request +/// +/// These parameters are provided in the request via URL query string. +/// Parameters taken by the "pki-vault" plugin to generate a unique key +/// for a sandbox store and retrieve credentials specific to the sandbox. +#[derive(Debug, PartialEq, serde::Deserialize)] +pub struct SandboxParams { + /// Required: ID of a sandbox or pod + pub id: String, + + /// Required: IP of a sandbox or pod + pub ip: String, + + /// Required: Name of a sandbox or pod + pub name: String, +} + +impl TryFrom<&str> for SandboxParams { + type Error = Error; + + fn try_from(query: &str) -> Result { + let params: SandboxParams = serde_qs::from_str(query)?; + Ok(params) + } +} + +/// Credentials necessary for initiating a server inside sandbox +#[derive(Debug, serde::Serialize)] +pub struct ServerCredential { + pub key: Vec, + pub cert: Vec, + pub ca_cert: Vec, +} + +/// Manages the credentials generation, handling requests +/// from backend, and credentials persistence storage +pub struct PKIVaultPlugin { + pub plugin_dir: PathBuf, + pub cert_details: PKIVaultCertDetails, + pub credblob_file: PathBuf, + pub cred_store: Arc>>, +} + +impl PKIVaultPlugin { + fn get_credential(&self, key: &str) -> Option { + let cred_store = self.cred_store.read().unwrap(); + cred_store.get(key).cloned() + } + + fn store_credential(&self, key: &str, credential: Credential) { + let mut cred_store = self.cred_store.write().unwrap(); + cred_store.insert(key.to_string(), credential); + } + + // Generate the credential (keys and certs for ca, server, and client) + fn generate_credential(&self, key: &str) -> Result> { + let credential = Credential::new(&self.cert_details)?; + + // Store the credential into the hashmap + self.store_credential(key, credential.clone()); + + // Write the hashmap to file for a persistence copy + if let Err(e) = self.save_hashmap(&self.credblob_file) { + log::warn!("Failed to store credentials into file: {}", e); + } + + log::info!("Returning newly generated credential!"); + let resource = ServerCredential { + key: credential.server_key.clone(), + cert: credential.server_cert.clone(), + ca_cert: credential.ca_cert.clone(), + }; + + Ok(serde_json::to_vec(&resource)?) + } + + fn save_hashmap(&self, path: &PathBuf) -> Result<()> { + let cred_store = self.cred_store.read().unwrap(); + let serialized = serde_json::to_string(&*cred_store)?; + fs::write(path, serialized)?; + Ok(()) + } + + async fn get_server_credential(&self, params: &SandboxParams) -> Result> { + // Return the server credential if the credential presents in the hashmap + let key = format!("{}_{}_{}", ¶ms.name, ¶ms.ip, ¶ms.id); + if let Some(credential) = self.get_credential(&key) { + log::info!("Returning existing credential!"); + + let resource = ServerCredential { + key: credential.server_key, + cert: credential.server_cert, + ca_cert: credential.ca_cert, + }; + + return Ok(serde_json::to_vec(&resource)?); + }; + + // Otherwise return newly generated credential + self.generate_credential(&key) + } +} + +#[async_trait::async_trait] +impl ClientPlugin for PKIVaultPlugin { + async fn handle( + &self, + _body: &[u8], + query: &str, + path: &str, + method: &Method, + ) -> Result> { + let sub_path = path + .strip_prefix('/') + .context("accessed path is illegal, should start with `/`")?; + if method.as_str() != "GET" { + bail!("Illegal HTTP method. Only GET is supported"); + } + + match sub_path { + "credential" => { + let params = SandboxParams::try_from(query)?; + let credential = self.get_server_credential(¶ms).await?; + + Ok(credential) + } + _ => Err(anyhow!("{} not supported", sub_path))?, + } + } + + async fn validate_auth( + &self, + _body: &[u8], + _query: &str, + _path: &str, + _method: &Method, + ) -> Result { + Ok(false) + } + + /// Whether the body needs to be encrypted via TEE key pair. + /// If returns `Ok(true)`, the KBS server will encrypt the whole body + /// with TEE key pair and use KBS protocol's Response format. + async fn encrypted( + &self, + _body: &[u8], + _query: &str, + _path: &str, + _method: &Method, + ) -> Result { + Ok(true) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio; + + #[tokio::test] + async fn test_handle() { + let config = PKIVaultPluginConfig::default(); + let plugin = PKIVaultPlugin::try_from(config).unwrap(); + + // Define sample inputs + let body: &[u8] = b""; + let query = "id=3367348&ip=60.11.12.43&name=pod7"; + let path = "/credential"; + let method = &Method::GET; + + // Act: call the handle method + let result = plugin.handle(body, query, path, method).await; + + // Assert: check the result + match result { + Ok(response) => { + // Expected results + let key = String::from("pod7_60.11.12.43_3367348"); + + if let Some(credential) = plugin.get_credential(&key) { + let resource = ServerCredential { + key: credential.server_key, + cert: credential.server_cert, + ca_cert: credential.ca_cert, + }; + + let expected_response = serde_json::to_vec(&resource).unwrap(); + assert_eq!(response, expected_response); + }; + } + Err(e) => panic!("Expected Ok, got Err: {:?}", e), + } + } +} diff --git a/kbs/src/plugins/implementations/pki_vault/mod.rs b/kbs/src/plugins/implementations/pki_vault/mod.rs new file mode 100644 index 0000000000..d4bef1d010 --- /dev/null +++ b/kbs/src/plugins/implementations/pki_vault/mod.rs @@ -0,0 +1,4 @@ +pub mod credential; +pub mod manager; + +pub use manager::{PKIVaultPlugin, PKIVaultPluginConfig}; diff --git a/kbs/src/plugins/implementations/splitapi/backend.rs b/kbs/src/plugins/implementations/splitapi/backend.rs deleted file mode 100644 index d9f5cdc246..0000000000 --- a/kbs/src/plugins/implementations/splitapi/backend.rs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2025 by IBM Corporation -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use anyhow::Result; -use serde::Deserialize; -use std::{path::PathBuf, sync::Arc}; - -use super::generator::CertificateDetails; -use super::manager; - -pub const CREDENTIALS_BLOB_FILE: &str = "certificates.json"; - -/// Services supported by the SplitAPI plugin -#[async_trait::async_trait] -pub trait SplitAPIBackend: Send + Sync { - /// Returns credentials for API Proxy server, generates if not exist - async fn get_server_credential(&self, params: &SandboxParams) -> Result>; -} - -pub struct SplitAPI { - pub backend: Arc, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(tag = "type")] -pub struct SplitAPIConfig { - pub plugin_dir: String, - #[serde(default)] - pub credential_blob_filename: String, - #[serde(default)] - pub certificate_details: CertificateDetails, -} - -impl Default for SplitAPIConfig { - fn default() -> Self { - Self { - plugin_dir: String::from(""), - credential_blob_filename: CREDENTIALS_BLOB_FILE.into(), - certificate_details: CertificateDetails::default(), - } - } -} - -impl TryFrom for SplitAPI { - type Error = anyhow::Error; - - fn try_from(config: SplitAPIConfig) -> anyhow::Result { - let backend = manager::CertManager::new( - PathBuf::from(&config.plugin_dir), - config.credential_blob_filename, - &config.certificate_details, - )?; - - Ok(Self { - backend: Arc::new(backend), - }) - } -} - -/// Parameters for the credential request -/// -/// These parameters are provided in the request via URL query string. -/// Parameters taken by the "splitapi" plugin to generate a unique key -/// for a sandbox store and retrieve credentials specific to the sandbox. -#[derive(Debug, PartialEq, serde::Deserialize)] -pub struct SandboxParams { - /// Required: ID of a sandbox or pod - pub id: String, - // Required: IP of a sandbox or pod - pub ip: String, - // Required: name of a sandbox or pod - pub name: String, -} - -impl TryFrom<&str> for SandboxParams { - type Error = anyhow::Error; - - fn try_from(query: &str) -> Result { - let params: SandboxParams = serde_qs::from_str(query)?; - Ok(params) - } -} diff --git a/kbs/src/plugins/implementations/splitapi/generator.rs b/kbs/src/plugins/implementations/splitapi/generator.rs deleted file mode 100644 index f2bf08c099..0000000000 --- a/kbs/src/plugins/implementations/splitapi/generator.rs +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright (c) 2025 by IBM Corporation -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use anyhow::{Context, Result}; -use openssl::asn1::Asn1Time; -use openssl::bn::BigNum; -use openssl::hash::MessageDigest; -use openssl::pkey::PKey; -use openssl::rsa::Rsa; -use openssl::x509::extension::AuthorityKeyIdentifier; -use openssl::x509::extension::BasicConstraints; -use openssl::x509::extension::KeyUsage; -use openssl::x509::extension::SubjectKeyIdentifier; -use openssl::x509::X509Builder; -use openssl::x509::{X509Name, X509NameBuilder, X509Req, X509ReqBuilder, X509}; -use serde::Deserialize; -use std::fs; -use std::fs::File; -use std::io::Read; -use std::io::Write; -use std::path::Path; -use std::path::PathBuf; -use tempfile::TempDir; - -use super::manager::Credentials; - -pub const CA_KEY_FILENAME: &str = "ca.key"; -pub const CA_CRT_FILENAME: &str = "ca.pem"; -pub const CLIENT_KEY_FILENAME: &str = "client.key"; -pub const CLIENT_CSR_FILENAME: &str = "client.csr"; -pub const CLIENT_CRT_FILENAME: &str = "client.pem"; -pub const SERVER_KEY_FILENAME: &str = "server.key"; -pub const SERVER_CSR_FILENAME: &str = "server.csr"; -pub const SERVER_CRT_FILENAME: &str = "server.pem"; - -const KEY_SIZE: u32 = 2048; - -/// Default certificate details if not configured -pub const DEFAULT_COUNTRY: &str = "AA"; -pub const DEFAULT_STATE: &str = "Default State"; -pub const DEFAULT_LOCALITY: &str = "Default City"; -pub const DEFAULT_ORGANIZATION: &str = "Default Organization"; -pub const DEFAULT_ORG_UNIT: &str = "Default Unit"; -pub const DEFAULT_CA_COMMON_NAME: &str = "grpc-tls CA"; -pub const DEFAULT_SERVER_COMMON_NAME: &str = "server"; -pub const DEFAULT_CLIENT_COMMON_NAME: &str = "client"; -pub const DEFAULT_CA_VALIDITY_DAYS: u32 = 3650; -pub const DEFAULT_SERVER_VALIDITY_DAYS: u32 = 180; -pub const DEFAULT_CLIENT_VALIDITY_DAYS: u32 = 180; - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(default)] -pub struct CertificateDetails { - pub country: String, - pub state: String, - pub locality: String, - pub organization: String, - pub org_unit: String, - pub ca: Certificate, - pub server: Certificate, - pub client: Certificate, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -pub struct Certificate { - pub common_name: String, - pub validity_days: u32, -} - -impl Default for CertificateDetails { - fn default() -> Self { - CertificateDetails { - country: DEFAULT_COUNTRY.to_string(), - state: DEFAULT_STATE.to_string(), - locality: DEFAULT_LOCALITY.to_string(), - organization: DEFAULT_ORGANIZATION.to_string(), - org_unit: DEFAULT_ORG_UNIT.to_string(), - ca: Certificate { - common_name: DEFAULT_CA_COMMON_NAME.to_string(), - validity_days: DEFAULT_CA_VALIDITY_DAYS, - }, - server: Certificate { - common_name: DEFAULT_SERVER_COMMON_NAME.to_string(), - validity_days: DEFAULT_SERVER_VALIDITY_DAYS, - }, - client: Certificate { - common_name: DEFAULT_CLIENT_COMMON_NAME.to_string(), - validity_days: DEFAULT_CLIENT_VALIDITY_DAYS, - }, - } - } -} - -#[derive(Debug)] -pub struct CredentialGenerator { - key_size: u32, - ca_key: PathBuf, - ca_crt: PathBuf, - client_key: PathBuf, - client_csr: PathBuf, - client_crt: PathBuf, - server_key: PathBuf, - server_csr: PathBuf, - server_crt: PathBuf, -} - -impl CredentialGenerator { - pub fn new(cred_dir: &TempDir) -> Result { - Ok(Self { - key_size: KEY_SIZE, - ca_key: cred_dir.path().to_owned().join(CA_KEY_FILENAME), - ca_crt: cred_dir.path().to_owned().join(CA_CRT_FILENAME), - client_key: cred_dir.path().to_owned().join(CLIENT_KEY_FILENAME), - client_csr: cred_dir.path().to_owned().join(CLIENT_CSR_FILENAME), - client_crt: cred_dir.path().to_owned().join(CLIENT_CRT_FILENAME), - server_key: cred_dir.path().to_owned().join(SERVER_KEY_FILENAME), - server_csr: cred_dir.path().to_owned().join(SERVER_CSR_FILENAME), - server_crt: cred_dir.path().to_owned().join(SERVER_CRT_FILENAME), - }) - } - - /// Run several steps for generate all the keys and certificates - pub fn generate(&self, cert_details: &CertificateDetails) -> Result { - // Create CA key, and self-signed certificate (valid for 10 years) - self.generate_private_key(self.ca_key.as_path(), self.key_size)?; - let ca_x509_name = self.build_x509_name( - &cert_details.ca.common_name, - &cert_details.country, - &cert_details.state, - &cert_details.locality, - &cert_details.organization, - &cert_details.org_unit, - )?; - self.generate_ca_cert( - self.ca_crt.as_path(), - self.ca_key.as_path(), - &ca_x509_name, - cert_details.ca.validity_days, - )?; - - // Create server key, csr, and certificate - self.generate_private_key(self.server_key.as_path(), self.key_size)?; - let server_x509_name = self.build_x509_name( - &cert_details.server.common_name, - &cert_details.country, - &cert_details.state, - &cert_details.locality, - &cert_details.organization, - &cert_details.org_unit, - )?; - self.generate_csr( - self.server_csr.as_path(), - self.server_key.as_path(), - &server_x509_name, - )?; - self.generate_cert( - self.server_crt.as_path(), - self.server_csr.as_path(), - self.ca_crt.as_path(), - self.ca_key.as_path(), - cert_details.server.validity_days, - )?; - - // Create client key, csr, and certificate - self.generate_private_key(self.client_key.as_path(), self.key_size)?; - let client_x509_name = self.build_x509_name( - &cert_details.client.common_name, - &cert_details.country, - &cert_details.state, - &cert_details.locality, - &cert_details.organization, - &cert_details.org_unit, - )?; - self.generate_csr( - self.client_csr.as_path(), - self.client_key.as_path(), - &client_x509_name, - )?; - self.generate_cert( - self.client_crt.as_path(), - self.client_csr.as_path(), - self.ca_crt.as_path(), - self.ca_key.as_path(), - cert_details.client.validity_days, - )?; - - // Read the generated credentials - let read_cred = - |path: &Path| fs::read(path).with_context(|| format!("read {}", path.display())); - - let credentials = Credentials { - ca_crt: read_cred(self.ca_crt.as_path())?, - client_key: read_cred(self.client_key.as_path())?, - client_crt: read_cred(self.client_crt.as_path())?, - server_key: read_cred(self.server_key.as_path())?, - server_crt: read_cred(self.server_crt.as_path())?, - }; - - Ok(credentials) - } - - fn generate_private_key(&self, ca_key_path: &Path, key_size: u32) -> Result<()> { - // Generate RSA key - let rsa = Rsa::generate(key_size).expect("Failed to generate RSA key"); - let pkey = PKey::from_rsa(rsa).expect("Failed to create PKey from RSA"); - - // Write the private key to a file - let private_key_pem = pkey.private_key_to_pem_pkcs8()?; - let mut file = File::create(ca_key_path)?; - file.write_all(&private_key_pem)?; - - Ok(()) - } - - fn build_x509_name( - &self, - common_name: &str, - country: &str, - state: &str, - locality: &str, - organization: &str, - org_unit: &str, - ) -> Result { - // Build X.509 name - let mut name_builder = X509NameBuilder::new()?; - name_builder.append_entry_by_text("C", country)?; - name_builder.append_entry_by_text("ST", state)?; - name_builder.append_entry_by_text("L", locality)?; - name_builder.append_entry_by_text("O", organization)?; - name_builder.append_entry_by_text("OU", org_unit)?; - name_builder.append_entry_by_text("CN", common_name)?; - let name = name_builder.build(); - - Ok(name) - } - - fn generate_ca_cert( - &self, - crt_path: &Path, - ca_key_path: &Path, - name: &X509Name, - validity_days: u32, - ) -> Result<()> { - // Read the private key from file - let mut file = File::open(ca_key_path)?; - let mut key_pem = Vec::new(); - file.read_to_end(&mut key_pem)?; - let rsa = Rsa::private_key_from_pem(&key_pem)?; - let pkey = PKey::from_rsa(rsa)?; - - // Build the X.509 certificate - let mut x509_builder = X509Builder::new()?; - x509_builder.set_subject_name(name)?; - x509_builder.set_issuer_name(name)?; - x509_builder.set_pubkey(&pkey)?; - - // Set certificate validity period - x509_builder - .set_not_before(&Asn1Time::days_from_now(0).expect("Failed to set not before")) - .expect("Failed to set not before"); - x509_builder - .set_not_after( - &Asn1Time::days_from_now(validity_days).expect("Failed to set not after"), - ) - .expect("Failed to set not after"); - - // Sign the certificate - x509_builder.sign(&pkey, MessageDigest::sha256())?; - let x509 = x509_builder.build(); - - // Write the certificate to a file - let crt_pem = x509.to_pem()?; - let mut crt_file = File::create(crt_path)?; - crt_file.write_all(&crt_pem)?; - - Ok(()) - } - - fn generate_csr( - &self, - csr_path: &Path, - private_key_path: &Path, - name: &X509Name, - ) -> Result<()> { - // Read the private key from file - let mut file = File::open(private_key_path)?; - let mut key_pem = Vec::new(); - file.read_to_end(&mut key_pem)?; - let rsa = Rsa::private_key_from_pem(&key_pem)?; - let pkey = PKey::from_rsa(rsa)?; - - // Create a new X.509 certificate signing request (CSR) - let mut csr_builder = X509ReqBuilder::new()?; - csr_builder.set_subject_name(name)?; - csr_builder.set_pubkey(&pkey)?; - csr_builder.sign(&pkey, MessageDigest::sha256())?; - - let csr = csr_builder.build(); - - // Write CSR to a file - let mut csr_file = File::create(csr_path)?; - csr_file.write_all(&csr.to_pem()?)?; - - Ok(()) - } - - fn generate_cert( - &self, - crt_path: &Path, - csr_path: &Path, - ca_crt_path: &Path, - ca_key_path: &Path, - validity_days: u32, - ) -> Result<()> { - // Step 1: Read the CSR - let mut csr_file = File::open(csr_path)?; - let mut csr_data = vec![]; - csr_file.read_to_end(&mut csr_data)?; - let csr = X509Req::from_pem(&csr_data)?; - - // Step 2: Read the CA PEM - let mut ca_file = File::open(ca_crt_path)?; - let mut ca_data = vec![]; - ca_file.read_to_end(&mut ca_data)?; - let ca_cert = X509::from_pem(&ca_data)?; - - // Step 3: Read the CA Key - let mut ca_key_file = File::open(ca_key_path)?; - let mut ca_key_data = vec![]; - ca_key_file.read_to_end(&mut ca_key_data)?; - let ca_key = PKey::private_key_from_pem(&ca_key_data)?; - - // Step 5: Create the server certificate - let mut builder = X509Builder::new()?; - - // Set the version of the certificate - builder.set_version(2)?; - - // Set the serial number - let serial_number = { - let mut serial = BigNum::new()?; - serial.rand(159, openssl::bn::MsbOption::MAYBE_ZERO, false)?; - serial.to_asn1_integer()? - }; - builder.set_serial_number(&serial_number)?; - - // Set the subject name from the CSR - builder.set_subject_name(csr.subject_name())?; - //TODO: add sandbox IP in the subject - - // Set the issuer name from the CA certificate - builder.set_issuer_name(ca_cert.subject_name())?; - - // Set the public key from the CSR - let public_key = csr.public_key()?; - builder.set_pubkey(&public_key)?; - - // Set the certificate validity period - let not_before = openssl::asn1::Asn1Time::days_from_now(0)?; - let not_after = openssl::asn1::Asn1Time::days_from_now(validity_days)?; - builder.set_not_before(¬_before)?; - builder.set_not_after(¬_after)?; - - // Add extensions from the certificate extensions file - builder.append_extension(BasicConstraints::new().critical().build()?)?; - builder.append_extension( - KeyUsage::new() - .digital_signature() - .key_encipherment() - .build()?, - )?; - builder.append_extension( - SubjectKeyIdentifier::new().build(&builder.x509v3_context(None, None))?, - )?; - builder.append_extension( - AuthorityKeyIdentifier::new() - .keyid(false) - .issuer(false) - .build(&builder.x509v3_context(Some(&ca_cert), None))?, - )?; - - // Sign the certificate with the CA key - builder.sign(&ca_key, MessageDigest::sha256())?; - - // Write the server certificate to a file - let server_crt = builder.build().to_pem()?; - let mut crt_file = File::create(crt_path)?; - crt_file.write_all(&server_crt)?; - - Ok(()) - } -} diff --git a/kbs/src/plugins/implementations/splitapi/manager.rs b/kbs/src/plugins/implementations/splitapi/manager.rs deleted file mode 100644 index 03f93f3a63..0000000000 --- a/kbs/src/plugins/implementations/splitapi/manager.rs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2025 by IBM Corporation -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - fs, - path::PathBuf, - sync::atomic::{AtomicBool, Ordering}, - sync::Arc, -}; -use tempfile::tempdir_in; -use tokio::sync::RwLock; - -use super::backend::{SandboxParams, SplitAPIBackend}; -use super::generator::{CertificateDetails, CredentialGenerator}; - -/// Credentials (keys and certs for CA, server, and client) -/// ncessary for the SplitAPI work -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Credentials { - pub ca_crt: Vec, - pub client_key: Vec, - pub client_crt: Vec, - pub server_key: Vec, - pub server_crt: Vec, -} - -/// Credentials necessary for SplitAPI proxy server -#[derive(Debug, serde::Serialize)] -pub struct ServerCredentials { - pub key: Vec, - pub crt: Vec, - pub ca_crt: Vec, -} - -/// Manages the credentials generation, handling requests -/// from backend, and credentials persistence storage -pub struct CertManager { - pub plugin_dir: PathBuf, - pub certificate_details: CertificateDetails, - pub credblob_file: PathBuf, //String, - pub state: Arc>>, - credential_loaded_from_file: AtomicBool, -} - -impl CertManager { - pub fn new( - plugin_dir: PathBuf, - blob_file: String, - cert_details: &CertificateDetails, - ) -> anyhow::Result { - if !plugin_dir.exists() { - fs::create_dir_all(&plugin_dir)?; - log::info!("plugin dir created = {}", plugin_dir.display()); - } - - let cblob_file = plugin_dir.as_path().join(blob_file); - - // Initialize the credential manager - Ok(Self { - plugin_dir, - certificate_details: cert_details.clone(), - credblob_file: cblob_file, - state: Arc::new(RwLock::new(HashMap::new())), - credential_loaded_from_file: AtomicBool::new(false), - }) - } - - async fn load_credentials(&self, key: &str) -> Option { - // Check if the credential is not loaded. If not, load them - if !self.credential_loaded_from_file.load(Ordering::SeqCst) { - if let Err(e) = self.load_from_file(&self.credblob_file).await { - log::warn!("Failed to load credentials from file: {}", e); - return None; - } - - // Update the flag, this is a one-time load until kbs restarts - self.credential_loaded_from_file - .store(true, Ordering::SeqCst); - } - - // Return the item from hashmap - let state = self.state.read().await; - state.get(key).cloned() - } - - async fn load_from_file(&self, path: &PathBuf) -> Result<()> { - let data = tokio::fs::read_to_string(&path).await?; - let deserialized: HashMap = serde_json::from_str(&data)?; - let mut state = self.state.write().await; - *state = deserialized; - Ok(()) - } - - async fn save_to_file(&self, path: &PathBuf) -> Result<()> { - let state = self.state.read().await; - let serialized = serde_json::to_string(&*state)?; - tokio::fs::write(path, serialized).await?; - Ok(()) - } -} - -#[async_trait::async_trait] -impl SplitAPIBackend for CertManager { - async fn get_server_credential(&self, params: &SandboxParams) -> Result> { - // Return the server credential if the credential presents in the hashmap - let key = format!("{}_{}_{}", ¶ms.name, ¶ms.ip, ¶ms.id); - if let Some(credentials) = self.load_credentials(&key).await { - log::info!("Returning already existed credentials!"); - - let resource = ServerCredentials { - key: credentials.server_key, - crt: credentials.server_crt, - ca_crt: credentials.ca_crt, - }; - - return Ok(serde_json::to_vec(&resource)?); - }; - - // Generate the credentials (keys and certs for ca, server, and client) - let credential_dir = tempdir_in(self.plugin_dir.as_path())?; - let generator = CredentialGenerator::new(&credential_dir)?; - let credentials = generator.generate(&self.certificate_details)?; - - log::info!("Credentials are generated!"); - - // Aquire the write lock and write the credential into the hashmap - { - let mut state = self.state.write().await; - state.insert(key, credentials.clone()); - } - - // Write the hashmap to file for a persistence copy - self.save_to_file(&self.credblob_file).await?; - - // Return the server credentials to respond the request - let resource = ServerCredentials { - key: credentials.server_key.clone(), - crt: credentials.server_crt.clone(), - ca_crt: credentials.ca_crt.clone(), - }; - - Ok(serde_json::to_vec(&resource)?) - } -} diff --git a/kbs/src/plugins/implementations/splitapi/mod.rs b/kbs/src/plugins/implementations/splitapi/mod.rs deleted file mode 100644 index 3084be8424..0000000000 --- a/kbs/src/plugins/implementations/splitapi/mod.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) 2025 by IBM Corporation -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -//! Splitapi plugin provisions credential resources for a sandbox and sends -//! sever specific credentials to the sandbox to initiate Split API proxy -//! server and establish a secure tunnel between tenant and the API proxy -//! server. - -pub mod generator; -pub mod manager; - -use actix_web::http::Method; -use anyhow::{anyhow, bail, Context, Result}; - -pub mod backend; -pub use backend::*; - -use super::super::plugin_manager::ClientPlugin; - -#[async_trait::async_trait] -impl ClientPlugin for SplitAPI { - async fn handle( - &self, - _body: &[u8], - query: &str, - path: &str, - method: &Method, - ) -> Result> { - let sub_path = path - .strip_prefix('/') - .context("accessed path is illegal, should start with `/`")?; - if method.as_str() != "GET" { - bail!("Illegal HTTP method. Only GET is supported"); - } - - match sub_path { - "credential" => { - let params = SandboxParams::try_from(query)?; - let credential = self.backend.get_server_credential(¶ms).await?; - - Ok(credential) - } - _ => Err(anyhow!("{} not supported", sub_path))?, - } - } - - async fn validate_auth( - &self, - _body: &[u8], - _query: &str, - _path: &str, - _method: &Method, - ) -> Result { - Ok(false) - } - - /// Whether the body needs to be encrypted via TEE key pair. - /// If returns `Ok(true)`, the KBS server will encrypt the whole body - /// with TEE key pair and use KBS protocol's Response format. - async fn encrypted( - &self, - _body: &[u8], - _query: &str, - _path: &str, - _method: &Method, - ) -> Result { - Ok(true) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::path::PathBuf; - use std::sync::Arc; - use tokio; - - use super::manager::{Credentials, ServerCredentials}; - - #[tokio::test] - async fn test_handle() { - let plugin_dir = "/opt/confidential-containers/kbs/plugin/splitapi"; - let config = SplitAPIConfig::default(); - let backend = manager::CertManager::new( - PathBuf::from(&plugin_dir), - config.credential_blob_filename, - &config.certificate_details, - ); - let backend = Arc::new(backend.expect("Failed to initialize backend")); - let split_api = SplitAPI { - backend: backend.clone(), - }; - - // Define sample inputs - let body: &[u8] = b""; - let query = "id=3367348&ip=60.11.12.43&name=pod7"; - let path = "/credential"; - let method = &Method::GET; - - // Act: call the handle method - let result = split_api.handle(body, query, path, method).await; - - // Assert: check the result - match result { - Ok(response) => { - // Expected results - let key = String::from("pod7_60.11.12.43_3367348"); - - let state = backend.state.read().await; - let credentials: Credentials = state - .get(&key) - .cloned() - .expect("Credentisl not found in hashmap"); - - let resource = ServerCredentials { - key: credentials.server_key, - crt: credentials.server_crt, - ca_crt: credentials.ca_crt, - }; - - let expected_response = serde_json::to_vec(&resource).unwrap(); - assert_eq!(response, expected_response); - } - Err(e) => panic!("Expected Ok, got Err: {:?}", e), - } - } -} diff --git a/kbs/src/plugins/plugin_manager.rs b/kbs/src/plugins/plugin_manager.rs index f841b16505..b18a57f7cc 100644 --- a/kbs/src/plugins/plugin_manager.rs +++ b/kbs/src/plugins/plugin_manager.rs @@ -12,8 +12,9 @@ use super::{sample, RepositoryConfig, ResourceStorage}; #[cfg(feature = "nebula-ca-plugin")] use super::{NebulaCaPlugin, NebulaCaPluginConfig}; -#[cfg(feature = "splitapi-plugin")] -use super::splitapi::{SplitAPI, SplitAPIConfig}; + +#[cfg(feature = "pki-vault-plugin")] +use super::{PKIVaultPlugin, PKIVaultPluginConfig}; #[cfg(feature = "pkcs11")] use super::{Pkcs11Backend, Pkcs11Config}; @@ -75,9 +76,10 @@ pub enum PluginsConfig { #[cfg(feature = "pkcs11")] #[serde(alias = "pkcs11")] Pkcs11(Pkcs11Config), - #[cfg(feature = "splitapi-plugin")] - #[serde(alias = "splitapi")] - SplitAPI(SplitAPIConfig), + + #[cfg(feature = "pki-vault-plugin")] + #[serde(alias = "pki_vault")] + PKIVaultPlugin(PKIVaultPluginConfig), } impl Display for PluginsConfig { @@ -91,6 +93,8 @@ impl Display for PluginsConfig { PluginsConfig::Pkcs11(_) => f.write_str("pkcs11"), #[cfg(feature = "splitapi-plugin")] PluginsConfig::SplitAPI(_) => f.write_str("splitapi"), + #[cfg(feature = "pki-vault-plugin")] + PluginsConfig::PKIVaultPlugin(_) => f.write_str("pki_vault"), } } } @@ -115,11 +119,12 @@ impl TryInto for PluginsConfig { let nebula_ca = NebulaCaPlugin::try_from(nebula_ca_config) .context("Initialize 'nebula-ca-plugin' failed")?; Arc::new(nebula_ca) as _ - #[cfg(feature = "splitapi-plugin")] - PluginsConfig::SplitAPI(splitapi_config) => { - let splitapi_plugin = SplitAPI::try_from(splitapi_config) - .context("Initialize 'SplitAPI' plugin failed")?; - Arc::new(splitapi_plugin) as _ + } + #[cfg(feature = "pki-vault-plugin")] + PluginsConfig::PKIVaultPlugin(pkivault_config) => { + let pkivault_plugin = PKIVaultPlugin::try_from(pkivault_config) + .context("Initialize 'PKI_Vault' plugin failed")?; + Arc::new(pkivault_plugin) as _ } #[cfg(feature = "pkcs11")] PluginsConfig::Pkcs11(pkcs11_config) => { From 407279f632984934d978062c2b766d93964ee6e3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 25 Jun 2025 04:55:04 +0000 Subject: [PATCH 4/5] Add endpoints for client (or owner) Updates include the following changes: - adding two endpoints (list_pods and get_client_credentials) - documentation of the pki_vault plugin (kbs/docs/pki_vault.md) - restructuring the pki_vault plugin code Signed-off-by: Salman Ahmed --- kbs/docs/pki_vault.md | 186 ++++++ kbs/src/plugins/implementations/pki_vault.rs | 590 ++++++++++++++++++ .../implementations/pki_vault/credential.rs | 272 -------- .../implementations/pki_vault/manager.rs | 267 -------- .../plugins/implementations/pki_vault/mod.rs | 4 - kbs/src/plugins/plugin_manager.rs | 8 +- 6 files changed, 779 insertions(+), 548 deletions(-) create mode 100644 kbs/docs/pki_vault.md create mode 100644 kbs/src/plugins/implementations/pki_vault.rs delete mode 100644 kbs/src/plugins/implementations/pki_vault/credential.rs delete mode 100644 kbs/src/plugins/implementations/pki_vault/manager.rs delete mode 100644 kbs/src/plugins/implementations/pki_vault/mod.rs diff --git a/kbs/docs/pki_vault.md b/kbs/docs/pki_vault.md new file mode 100644 index 0000000000..0914023eb8 --- /dev/null +++ b/kbs/docs/pki_vault.md @@ -0,0 +1,186 @@ +# PKI Vault plugin + +This plugin currently generates credentials (keys and certificates) for a server running inside the confidential VM (aka sandbox) and for the workload owner (who acts as a client for the server). The current design of the plugin prioritizes the mutual authentication between the server and the client. Such design is necessary for the SplitAPI (kata-containers/kata-containers#9159 and +kata-containers/kata-containers#9752) and peer-pods. + +This plugin also delivers the server-specific credentials to the sandbox (i.e., confidential PODs or VMs), specifically to the kata agent to initiate the server. The workload owner can communicate with the server using a secure tunnel. + +The server-specific credentials can be obtained throught the `get-resource` APIs by specifying the `plugin-name`. Currently, the plugin requires that the sandbox or `kata-agent` sends the IPv4 address, name, and the ID of the sandbox (i.e., pod) as part of the query string to obtain the credentials from the KBS. + +After receiving the credential request, the `pki_vault` plugin will create a CA, a key pair for the server and another key pair for client, and sign them using the self-signed CA. Currently, the generated credentials are stored in a hashmap with a unique key for each sandbox based on its name, ID, and IP address. But we expect this design will be changed in future. PKI Vault plugin responds to a request from the sandbox by sending the server specific credentials (key, cert) along with the CA certificate. A request from workload owner gets the client specific credentials (key, cert) and CA's certificate. + +# Setup + +1. Build the KBS with the cargo feature `pki-vault-plugin` enabled. + +```bash +make background-check-kbs POLICY_ENGINE=opa PKI_VAULT_PLUGIN=true +``` + +2. Configure the `pki-vault` plugin. Simply specifying the plugin name should be enough for the configuration, just add the lines below to the [KBS config](#kbs/config/docker-compose/kbs-config.toml). But one can specify addition details to the configuration file. + +```toml +[[plugins]] +name = "pki_vault" +``` +The following addition details can be set to the configuration file. +```toml +[[plugins]] +name = "pki_vault" +#plugin_dir = "/opt/confidential-containers/kbs/plugin/splitapi" +#cred_filename = "certificates.json" +[plugins.pkivault_cert_details] +country = "AA" +#state = "Default State" +#locality = "Default City" +organization = "Default Organization" +org_unit = "Default Unit" + +[plugins.pkivault_cert_details.ca] +#common_name = "grpc-tls CA" +validity_days = 3650 + +[plugins.pkivault_cert_details.server] +#common_name = "server" +#validity_days = 180 + +[plugins.pkivault_cert_details.client] +common_name = "client" +validity_days = 180 +``` + +3. Start trustee + +```bash +sudo ../target/release/kbs --config-file ./config/kbs-config.toml +``` +# Design choices [To be updated] + +## 1. Separate CA for each server (sandbox) +## 2. Single CA for all +## 3. Persistence vs. non-persistence + +# Runtime services + +All runtime services for both the server and client supported are described in the following sections. + +## Credentials service for server (i.e., pod or sandbox) + +Request the credentials for initiating a server inside a pod or sandbox. + +Only `GET` request is supported, e.g. `GET /kbs/v0/pki_vault/credentials?id=3367&ip=60.11.12.89&name=pod51`. Current the `GET` takes `id`, `ip`, and `name` parameters, but expect this parameters to be changed in the future design for supporting more generic use cases. + +The request takes parameters via URL query string. All parameters supported are described in the table below. Note currently, all parameters are required. + +| Property | Type | Required | Description | Default | Example | +|---------------------|--------|----------|-------------------------|---------|-------------------------------------------| +| `name` | String | Yes | Name of the pod | | `credentials?id=3367&ip=60.11.12.89&name=pod51` | +| `ip` | String | Yes | IPv4 address of a pod to assign to the certificate | | `credentials?id=3367&ip=60.11.12.89&name=pod51` | +| `id` | String | Yes | Pod ID | | `credentials?id=3367&ip=60.11.12.89&name=pod51` | + +The request will be processed only if the node passes the attestation, otherwise an error is returned. If the credentials for the server already already exists in the KBS, the plugin simply returns the existing credentials. Otherwise, the plugin generates the credentials. + +Once the request is processed, the following structure is returned in JSON format. + +```rust +pub struct CredentialsOut { + pub key: Vec, // Key created + pub cert: Vec, // Self-signed certificate created + pub ca_cert: Vec, // CA certificate +} +``` + + + +## Credentials services for client (i.e., workload owner) + +Request the client credentials for initiating a mutual TLS communication channel between client (workload owner) and the server (running inside a pod or sandbox). + +### `list_pods` +Only `GET` request is supported, e.g. `GET /kbs/v0/pki_vault/list_pods`. Here, `list_pods` is an API that the client (or workload owner) can invoke to get the list of pod names and their additional information. + +### `get_client_credentials` +Only `GET` request is supported, e.g. `GET /kbs/v0/pki_vault/get_client_credentials?id=3367&ip=60.11.12.89&name=pod51`. Here, `get_client_credentials` is an API that the client (or workload owner) can invoke to get the owner or client-specific credentails for a pod. A pod obtains the server-specific credentials through the `get_resource` API. The `get_client_credentials` API need a `query` parameter to indicate the target pod. + +All APIs supported are described in the table below. + +| API | Description | Example | +|---------------------|-------------------------|-------------------------------------------| +| `list_pods` | To get the list of pods that have server credentials created| `kbs/v0/pki_vault/list_pods` | +| `get_client_credentials` | To get the client credentials for a pod | `/kbs/v0/pki_vault/get_client_credentials?id=3367&ip=60.11.12.89&name=pod51` | + +The request will be processed only if the request is authenticated, otherwise an error is returned. In the current design, the credentials for the client already already exists in the KBS as they have been already created as part of the response of server credential request, so the plugin simply returns the existing client credentials. + +Once the request is processed, the following structure is returned in JSON format. + +```rust +pub struct CredentialsOut { + pub key: Vec, // Key created + pub cert: Vec, // Self-signed certificate created + pub ca_cert: Vec, // CA certificate +} +``` + + +# How to test (client or owner-side code) +In order to test the `pki-vault` plugin, we need to enable the plugin support in the `kbs_protocol` inside `attestation-agent` of the `guest-components` respository. The plugin support has been enabled in the `pki-vault-test` branch of following `guest-components` repository. + +`https://github.com/salmanyam/guest-components/tree/pki-vault-test` + +In addition to that, we need to patch the `kbs-client` located inside `tools` of `trustee`, so that the `kbs-client` can invoke the plugin call. A patched version of the `kbs-client` can be located in the `pki-vault-test` branch of the following `trustee` repository. + +`https://github.com/salmanyam/trustee/commits/pki-vault-test/` + +This patched `kbs-client` has also used the patched `guest-component` with the modified `kbs_protocol`. + +``` +kbs_protocol = { git = "https://github.com/salmanyam/guest-components.git", rev = "281be58d49e91a13a1c55fb30324c705f9d0d9c5", default-features = false } +kms = { git = "https://github.com/salmanyam/guest-components.git", rev = "281be58d49e91a13a1c55fb30324c705f9d0d9c5", default-features = false } +``` + +Once the `kbs-client` is built, use the `kbs-client` to make a request to KBS for generating and providing the server specific credentials. + +## Step-by-step instructions + +Pull the patched `kbs-client` code +``` +$ git clone https://github.com/salmanyam/trustee.git +$ cd trustee +$ git checkout pki-vault-test +``` + +Build the `kbs-client` code +``` +$ cd kbs && make cli +``` +> **Note:** To specify an attester, please use the `ATTESTER` environment variable, e.g., `ATTESTER=snp-attester` + + + +To invoke the `get_resource` API. KBS should return the server specific credentials such as server key, server certificates, and the certificate of the CA. +``` +$ sudo ../target/release/kbs-client --url http://127.0.0.1:8080 get-resource --plugin-name "pki_vault" --resource-path "credentials?id=3367353&ip=60.11.12.48&name=pod33" | base64 -d +``` + + +To invoke the `list_pods` API. This should return the list of pod names and associated information. + +``` +$ sudo ../target/release/kbs-client --url http://127.0.0.1:8080 pki-vault-config --auth-private-key config/private.key list-pods +``` + + + +To invoke the `get_client_credentials` API. This should return the client credentials of the following format. +``` +$ sudo ../target/release/kbs-client --url http://127.0.0.1:8080 pki-vault-config --auth-private-key config/private.key get-client-credentials --query "name=pod51&ip=60.11.12.89&id=3367383" +``` + + +``` +pub struct Credentials { + pub key: Vec, + pub cert: Vec, + pub ca_cert: Vec, +} +``` diff --git a/kbs/src/plugins/implementations/pki_vault.rs b/kbs/src/plugins/implementations/pki_vault.rs new file mode 100644 index 0000000000..338d8a5971 --- /dev/null +++ b/kbs/src/plugins/implementations/pki_vault.rs @@ -0,0 +1,590 @@ +// Copyright (c) 2025 by IBM Corporation +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use actix_web::http::Method; +use anyhow::{anyhow, bail, Context, Error, Result}; +use std::sync::RwLock; +use std::{collections::HashMap, fs, path::PathBuf, sync::Arc}; + +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Private}; +use openssl::x509::{ + extension::{AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectKeyIdentifier}, + X509Builder, X509Name, X509NameBuilder, X509, +}; +use serde::{Deserialize, Serialize}; + +use crate::plugins::plugin_manager::ClientPlugin; + +const DEFAULT_PLUGIN_DIR: &str = "/opt/confidential-containers/kbs/plugin/pki_vault"; +const DEFAULT_CREDENTIALS_BLOB_FILE: &str = "certificates.json"; + +/// Default certificate details if not configured +pub const DEFAULT_COUNTRY: &str = "AA"; +pub const DEFAULT_STATE: &str = "Default State"; +pub const DEFAULT_LOCALITY: &str = "Default City"; +pub const DEFAULT_ORGANIZATION: &str = "Default Organization"; +pub const DEFAULT_ORG_UNIT: &str = "Default Unit"; +pub const DEFAULT_CA_COMMON_NAME: &str = "grpc-tls CA"; +pub const DEFAULT_SERVER_COMMON_NAME: &str = "server"; +pub const DEFAULT_CLIENT_COMMON_NAME: &str = "client"; +pub const DEFAULT_CA_VALIDITY_DAYS: u32 = 3650; +pub const DEFAULT_SERVER_VALIDITY_DAYS: u32 = 180; +pub const DEFAULT_CLIENT_VALIDITY_DAYS: u32 = 180; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct PKIVaultCertDetails { + /// Two-letter country code represents the country in which the entity resides + pub country: String, + /// State or province where the entity is located + pub state: String, + /// Locality or city where the entity is located + pub locality: String, + /// Organization or company name + pub organization: String, + /// An organizational unit within the organization + pub org_unit: String, + /// Information regarding the CA certificate + pub ca: CaCrtDetails, + /// Information regarding the server certificate + pub server: ServerCrtDetails, + /// Information regarding the client certificate + pub client: ClientCrtDetails, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct CaCrtDetails { + pub common_name: String, + pub validity_days: u32, +} + +impl Default for CaCrtDetails { + fn default() -> Self { + CaCrtDetails { + common_name: DEFAULT_CA_COMMON_NAME.to_string(), + validity_days: DEFAULT_CA_VALIDITY_DAYS, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct ServerCrtDetails { + pub common_name: String, + pub validity_days: u32, +} + +impl Default for ServerCrtDetails { + fn default() -> Self { + ServerCrtDetails { + common_name: DEFAULT_SERVER_COMMON_NAME.to_string(), + validity_days: DEFAULT_SERVER_VALIDITY_DAYS, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct ClientCrtDetails { + pub common_name: String, + pub validity_days: u32, +} + +impl Default for ClientCrtDetails { + fn default() -> Self { + ClientCrtDetails { + common_name: DEFAULT_CLIENT_COMMON_NAME.to_string(), + validity_days: DEFAULT_CLIENT_VALIDITY_DAYS, + } + } +} + +impl Default for PKIVaultCertDetails { + fn default() -> Self { + PKIVaultCertDetails { + country: DEFAULT_COUNTRY.to_string(), + state: DEFAULT_STATE.to_string(), + locality: DEFAULT_LOCALITY.to_string(), + organization: DEFAULT_ORGANIZATION.to_string(), + org_unit: DEFAULT_ORG_UNIT.to_string(), + ca: CaCrtDetails::default(), + server: ServerCrtDetails::default(), + client: ClientCrtDetails::default(), + } + } +} + +/// Credentials necessary for mutual TLS communication +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Credentials { + pub ca_cert: Vec, + pub server_key: Vec, + pub server_cert: Vec, + pub client_key: Vec, + pub client_cert: Vec, +} + +impl Credentials { + pub fn new(cert_details: &PKIVaultCertDetails) -> Result { + // Private keys for CA, Server, and Client + let ca_private_key = PKey::generate_ed25519()?; + let server_private_key = PKey::generate_ed25519()?; + let client_private_key = PKey::generate_ed25519()?; + + // Generate CA certificate + let ca_cert = Self::generate_ca_cert(&ca_private_key, cert_details)?; + + // Generate certificate for Server + let server_cert = Self::generate_signed_cert( + &server_private_key, + &ca_cert, + &ca_private_key, + cert_details, + "server", + )?; + + // Generate certificate for Client + let client_cert = Self::generate_signed_cert( + &client_private_key, + &ca_cert, + &ca_private_key, + cert_details, + "client", + )?; + + Ok(Self { + ca_cert: ca_cert.to_pem()?, + server_key: server_private_key.private_key_to_pem_pkcs8()?, + server_cert: server_cert.to_pem()?, + client_key: client_private_key.private_key_to_pem_pkcs8()?, + client_cert: client_cert.to_pem()?, + }) + } + + fn generate_signed_cert( + private_key: &PKey, + ca_cert: &X509, + ca_private_key: &PKey, + cert_details: &PKIVaultCertDetails, + server_or_client: &str, + ) -> Result { + // Check if the certificate is for server or client + let (common_name, validity_days) = if server_or_client == "server" { + ( + &cert_details.server.common_name, + cert_details.server.validity_days, + ) + } else { + ( + &cert_details.client.common_name, + cert_details.client.validity_days, + ) + }; + + // Build the x509 name + let name = Self::build_x509_name( + common_name, + &cert_details.country, + &cert_details.state, + &cert_details.locality, + &cert_details.organization, + &cert_details.org_unit, + )?; + + let mut x509_builder = X509Builder::new()?; + x509_builder.set_version(2)?; + x509_builder.set_subject_name(&name)?; + x509_builder.set_issuer_name(ca_cert.subject_name())?; + x509_builder.set_pubkey(private_key)?; + + let serial_number = BigNum::from_u32(2)?.to_asn1_integer()?; + x509_builder.set_serial_number(&serial_number)?; + x509_builder.set_not_before(Asn1Time::days_from_now(0)?.as_ref())?; + x509_builder.set_not_after(Asn1Time::days_from_now(validity_days)?.as_ref())?; + + // Add extensions from the certificate extensions file + x509_builder.append_extension(BasicConstraints::new().critical().build()?)?; + x509_builder.append_extension( + KeyUsage::new() + .digital_signature() + .key_encipherment() + .build()?, + )?; + x509_builder.append_extension( + SubjectKeyIdentifier::new().build(&x509_builder.x509v3_context(None, None))?, + )?; + x509_builder.append_extension( + AuthorityKeyIdentifier::new() + .keyid(false) + .issuer(false) + .build(&x509_builder.x509v3_context(Some(ca_cert), None))?, + )?; + + x509_builder.sign(ca_private_key, MessageDigest::null())?; + Ok(x509_builder.build()) + } + + fn generate_ca_cert( + ca_private_key: &PKey, + cert_details: &PKIVaultCertDetails, + ) -> Result { + // Build the x509 name + let name = Self::build_x509_name( + &cert_details.ca.common_name, + &cert_details.country, + &cert_details.state, + &cert_details.locality, + &cert_details.organization, + &cert_details.org_unit, + )?; + + // Build the X.509 certificate + let mut x509_builder = X509Builder::new()?; + x509_builder.set_subject_name(&name)?; + x509_builder.set_issuer_name(&name)?; + x509_builder.set_pubkey(ca_private_key)?; + + // Set certificate validity period + x509_builder.set_not_before(Asn1Time::days_from_now(0)?.as_ref())?; + x509_builder + .set_not_after(Asn1Time::days_from_now(cert_details.ca.validity_days)?.as_ref())?; + + // Sign the certificate + x509_builder.sign(ca_private_key, MessageDigest::null())?; + Ok(x509_builder.build()) + } + + fn build_x509_name( + common_name: &str, + country: &str, + state: &str, + locality: &str, + organization: &str, + org_unit: &str, + ) -> Result { + let mut name_builder = X509NameBuilder::new()?; + name_builder.append_entry_by_text("C", country)?; + name_builder.append_entry_by_text("ST", state)?; + name_builder.append_entry_by_text("L", locality)?; + name_builder.append_entry_by_text("O", organization)?; + name_builder.append_entry_by_text("OU", org_unit)?; + name_builder.append_entry_by_text("CN", common_name)?; + let name = name_builder.build(); + + Ok(name) + } +} + + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct PKIVaultPluginConfig { + pub plugin_dir: String, + pub cred_filename: String, + pub pkivault_cert_details: PKIVaultCertDetails, +} + +impl Default for PKIVaultPluginConfig { + fn default() -> Self { + PKIVaultPluginConfig { + plugin_dir: DEFAULT_PLUGIN_DIR.into(), + cred_filename: DEFAULT_CREDENTIALS_BLOB_FILE.into(), + pkivault_cert_details: PKIVaultCertDetails::default(), + } + } +} + +impl TryFrom for PKIVaultPlugin { + type Error = Error; + + fn try_from(config: PKIVaultPluginConfig) -> Result { + // Create the plugin dir if it does not exist + let plugin_dir = PathBuf::from(&config.plugin_dir); + if !plugin_dir.exists() { + fs::create_dir_all(&plugin_dir)?; + log::info!("plugin dir created = {}", plugin_dir.display()); + } + + // Read the existing credentials from file + let path = PathBuf::from(&config.plugin_dir) + .as_path() + .join(config.cred_filename); + + let credential: HashMap = if path.exists() { + match fs::read_to_string(&path) { + Ok(data) => serde_json::from_str(&data).unwrap_or_else(|_| HashMap::new()), + Err(_) => { + log::warn!("Error reading the credentials file."); + HashMap::new() + } + } + } else { + log::warn!("Credentails file does not exist."); + HashMap::new() + }; + + // Initializing the PKI Vault plugin with existing credentials data from file + Ok(PKIVaultPlugin { + plugin_dir: PathBuf::from(&config.plugin_dir), + cert_details: config.pkivault_cert_details, + credblob_file: path, + cred_store: Arc::new(RwLock::new(credential)), + }) + } +} + +/// Parameters for the credentials request +/// +/// These parameters are provided in the request via URL query string. +/// Parameters taken by the "pki-vault" plugin to generate a unique key +/// for a sandbox store and retrieve credentials specific to the sandbox. +#[derive(Debug, PartialEq, serde::Deserialize)] +pub struct SandboxParams { + /// Required: ID of a sandbox or pod + pub id: String, + + /// Required: IP of a sandbox or pod + pub ip: String, + + /// Required: Name of a sandbox or pod + pub name: String, +} + +impl TryFrom<&str> for SandboxParams { + type Error = Error; + + fn try_from(query: &str) -> Result { + let params: SandboxParams = serde_qs::from_str(query)?; + Ok(params) + } +} + +/// Credentials necessary for secure server-client communication +#[derive(Debug, serde::Serialize)] +pub struct CredentialsOut { + pub key: Vec, + pub cert: Vec, + pub ca_cert: Vec, +} + +/// Manages the credentials generation, handling requests +/// from backend, and credentials persistence storage +pub struct PKIVaultPlugin { + pub plugin_dir: PathBuf, + pub cert_details: PKIVaultCertDetails, + pub credblob_file: PathBuf, + pub cred_store: Arc>>, +} + +impl PKIVaultPlugin { + fn get_credentials(&self, key: &str) -> Option { + let cred_store = self.cred_store.read().unwrap(); + cred_store.get(key).cloned() + } + + fn store_credentials(&self, key: &str, credentials: Credentials) { + let mut cred_store = self.cred_store.write().unwrap(); + cred_store.insert(key.to_string(), credentials); + } + + // Generate the credentials (keys and certs for ca, server, and client) + fn generate_credentials(&self, key: &str) -> Result> { + let credentials = Credentials::new(&self.cert_details)?; + + // Store the credentials into the hashmap + self.store_credentials(key, credentials.clone()); + + // Write the hashmap to file for a persistence copy + if let Err(e) = self.save_hashmap(&self.credblob_file) { + log::warn!("Failed to store credentials into file: {}", e); + } + + log::info!("Returning newly generated credentials!"); + let resource = CredentialsOut { + key: credentials.server_key.clone(), + cert: credentials.server_cert.clone(), + ca_cert: credentials.ca_cert.clone(), + }; + + Ok(serde_json::to_vec(&resource)?) + } + + fn save_hashmap(&self, path: &PathBuf) -> Result<()> { + let cred_store = self.cred_store.read().unwrap(); + let serialized = serde_json::to_string(&*cred_store)?; + fs::write(path, serialized)?; + Ok(()) + } + + async fn get_server_credentials(&self, params: &SandboxParams) -> Result> { + // Return the server credentials if the credentials presents in the hashmap + let key = format!("{}_{}_{}", ¶ms.name, ¶ms.ip, ¶ms.id); + if let Some(credentials) = self.get_credentials(&key) { + log::info!("Returning existing credentials!"); + + let resource = CredentialsOut { + key: credentials.server_key, + cert: credentials.server_cert, + ca_cert: credentials.ca_cert, + }; + + return Ok(serde_json::to_vec(&resource)?); + }; + + // Otherwise return newly generated credentials + self.generate_credentials(&key) + } + + async fn list_pods(&self) -> Result> { + let cred_store = self.cred_store.read().unwrap(); + let keys: Vec = cred_store.keys().cloned().collect(); + Ok(serde_json::to_vec(&keys).expect("Failed to deserialize it!")) + } + + async fn get_client_credentials(&self, params: &SandboxParams) -> Result> { + let key = format!("{}_{}_{}", ¶ms.name, ¶ms.ip, ¶ms.id); + if let Some(credentials) = self.get_credentials(&key) { + log::info!("Found client credentials!"); + + let resource = CredentialsOut { + key: credentials.client_key, + cert: credentials.client_cert, + ca_cert: credentials.ca_cert, + }; + + return Ok(serde_json::to_vec(&resource)?); + }; + + return Ok(serde_json::to_vec("")?); + } +} + +#[async_trait::async_trait] +impl ClientPlugin for PKIVaultPlugin { + async fn handle( + &self, + _body: &[u8], + query: &str, + path: &str, + method: &Method, + ) -> Result> { + let sub_path = path + .strip_prefix('/') + .context("accessed path is illegal, should start with `/`")?; + + match method.as_str() { + "GET" => { + match sub_path { + "credentials" => { + let params = SandboxParams::try_from(query)?; + let credentials = self.get_server_credentials(¶ms).await?; + + Ok(credentials) + } + "list_pods" => { + let pods = self.list_pods().await?; + + Ok(pods) + } + "get_client_credentials" => { + let params = SandboxParams::try_from(query)?; + let credentials = self.get_client_credentials(¶ms).await?; + + Ok(credentials) + } + _ => Err(anyhow!("{} not supported", sub_path))?, + } + } + _ => bail!("Illegal HTTP method. Only supports `GET` and `POST`"), + } + } + + async fn validate_auth( + &self, + _body: &[u8], + _query: &str, + path: &str, + method: &Method, + ) -> Result { + let sub_path = path + .strip_prefix('/') + .context("accessed path is illegal, should start with `/`")?; + + if method.as_str() == "GET" { + if sub_path != "credentials" { + return Ok(true); + } + } + + Ok(false) + } + + /// Whether the body needs to be encrypted via TEE key pair. + /// If returns `Ok(true)`, the KBS server will encrypt the whole body + /// with TEE key pair and use KBS protocol's Response format. + async fn encrypted( + &self, + _body: &[u8], + _query: &str, + path: &str, + method: &Method, + ) -> Result { + let sub_path = path + .strip_prefix('/') + .context("accessed path is illegal, should start with `/`")?; + + if method.as_str() == "GET" { + if sub_path == "credentials" { + return Ok(true); + } + } + Ok(false) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use tokio; + + #[tokio::test] + async fn test_handle() { + let config = PKIVaultPluginConfig::default(); + let plugin = PKIVaultPlugin::try_from(config).unwrap(); + + // Define sample inputs + let body: &[u8] = b""; + let query = "id=3367348&ip=60.11.12.43&name=pod7"; + let path = "/credentials"; + let method = &Method::GET; + + // Act: call the handle method + let result = plugin.handle(body, query, path, method).await; + + // Assert: check the result + match result { + Ok(response) => { + // Expected results + let key = String::from("pod7_60.11.12.43_3367348"); + + if let Some(credentials) = plugin.get_credentials(&key) { + let resource = CredentialsOut { + key: credentials.server_key, + cert: credentials.server_cert, + ca_cert: credentials.ca_cert, + }; + + let expected_response = serde_json::to_vec(&resource).unwrap(); + assert_eq!(response, expected_response); + }; + } + Err(e) => panic!("Expected Ok, got Err: {:?}", e), + } + } +} \ No newline at end of file diff --git a/kbs/src/plugins/implementations/pki_vault/credential.rs b/kbs/src/plugins/implementations/pki_vault/credential.rs deleted file mode 100644 index 7585158bdc..0000000000 --- a/kbs/src/plugins/implementations/pki_vault/credential.rs +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright (c) 2025 by IBM Corporation -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use anyhow::Result; -use openssl::asn1::Asn1Time; -use openssl::bn::BigNum; -use openssl::hash::MessageDigest; -use openssl::pkey::{PKey, Private}; -use openssl::x509::{ - extension::{AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectKeyIdentifier}, - X509Builder, X509Name, X509NameBuilder, X509, -}; -use serde::{Deserialize, Serialize}; - -/// Default certificate details if not configured -pub const DEFAULT_COUNTRY: &str = "AA"; -pub const DEFAULT_STATE: &str = "Default State"; -pub const DEFAULT_LOCALITY: &str = "Default City"; -pub const DEFAULT_ORGANIZATION: &str = "Default Organization"; -pub const DEFAULT_ORG_UNIT: &str = "Default Unit"; -pub const DEFAULT_CA_COMMON_NAME: &str = "grpc-tls CA"; -pub const DEFAULT_SERVER_COMMON_NAME: &str = "server"; -pub const DEFAULT_CLIENT_COMMON_NAME: &str = "client"; -pub const DEFAULT_CA_VALIDITY_DAYS: u32 = 3650; -pub const DEFAULT_SERVER_VALIDITY_DAYS: u32 = 180; -pub const DEFAULT_CLIENT_VALIDITY_DAYS: u32 = 180; - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(default)] -pub struct PKIVaultCertDetails { - /// Two-letter country code represents the country in which the entity resides - pub country: String, - /// State or province where the entity is located - pub state: String, - /// Locality or city where the entity is located - pub locality: String, - /// Organization or company name - pub organization: String, - /// An organizational unit within the organization - pub org_unit: String, - /// Information regarding the CA certificate - pub ca: CaCrtDetails, - /// Information regarding the server certificate - pub server: ServerCrtDetails, - /// Information regarding the client certificate - pub client: ClientCrtDetails, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(default)] -pub struct CaCrtDetails { - pub common_name: String, - pub validity_days: u32, -} - -impl Default for CaCrtDetails { - fn default() -> Self { - CaCrtDetails { - common_name: DEFAULT_CA_COMMON_NAME.to_string(), - validity_days: DEFAULT_CA_VALIDITY_DAYS, - } - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(default)] -pub struct ServerCrtDetails { - pub common_name: String, - pub validity_days: u32, -} - -impl Default for ServerCrtDetails { - fn default() -> Self { - ServerCrtDetails { - common_name: DEFAULT_SERVER_COMMON_NAME.to_string(), - validity_days: DEFAULT_SERVER_VALIDITY_DAYS, - } - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(default)] -pub struct ClientCrtDetails { - pub common_name: String, - pub validity_days: u32, -} - -impl Default for ClientCrtDetails { - fn default() -> Self { - ClientCrtDetails { - common_name: DEFAULT_CLIENT_COMMON_NAME.to_string(), - validity_days: DEFAULT_CLIENT_VALIDITY_DAYS, - } - } -} - -impl Default for PKIVaultCertDetails { - fn default() -> Self { - PKIVaultCertDetails { - country: DEFAULT_COUNTRY.to_string(), - state: DEFAULT_STATE.to_string(), - locality: DEFAULT_LOCALITY.to_string(), - organization: DEFAULT_ORGANIZATION.to_string(), - org_unit: DEFAULT_ORG_UNIT.to_string(), - ca: CaCrtDetails::default(), - server: ServerCrtDetails::default(), - client: ClientCrtDetails::default(), - } - } -} - -/// Credential necessary for mutual TLS communication -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Credential { - pub ca_cert: Vec, - pub server_key: Vec, - pub server_cert: Vec, - pub client_key: Vec, - pub client_cert: Vec, -} - -impl Credential { - pub fn new(cert_details: &PKIVaultCertDetails) -> Result { - // Private keys for CA, Server, and Client - let ca_private_key = PKey::generate_ed25519()?; - let server_private_key = PKey::generate_ed25519()?; - let client_private_key = PKey::generate_ed25519()?; - - // Generate CA certificate - let ca_cert = Self::generate_ca_cert(&ca_private_key, cert_details)?; - - // Generate certificate for Server - let server_cert = Self::generate_signed_cert( - &server_private_key, - &ca_cert, - &ca_private_key, - cert_details, - "server", - )?; - - // Generate certificate for Client - let client_cert = Self::generate_signed_cert( - &client_private_key, - &ca_cert, - &ca_private_key, - cert_details, - "client", - )?; - - Ok(Self { - ca_cert: ca_cert.to_pem()?, - server_key: server_private_key.private_key_to_pem_pkcs8()?, - server_cert: server_cert.to_pem()?, - client_key: client_private_key.private_key_to_pem_pkcs8()?, - client_cert: client_cert.to_pem()?, - }) - } - - fn generate_signed_cert( - private_key: &PKey, - ca_cert: &X509, - ca_private_key: &PKey, - cert_details: &PKIVaultCertDetails, - server_or_client: &str, - ) -> Result { - // Check if the certificate is for server or client - let (common_name, validity_days) = if server_or_client == "server" { - ( - &cert_details.server.common_name, - cert_details.server.validity_days, - ) - } else { - ( - &cert_details.client.common_name, - cert_details.client.validity_days, - ) - }; - - // Build the x509 name - let name = Self::build_x509_name( - common_name, - &cert_details.country, - &cert_details.state, - &cert_details.locality, - &cert_details.organization, - &cert_details.org_unit, - )?; - - let mut x509_builder = X509Builder::new()?; - x509_builder.set_version(2)?; - x509_builder.set_subject_name(&name)?; - x509_builder.set_issuer_name(ca_cert.subject_name())?; - x509_builder.set_pubkey(private_key)?; - - let serial_number = BigNum::from_u32(2)?.to_asn1_integer()?; - x509_builder.set_serial_number(&serial_number)?; - x509_builder.set_not_before(Asn1Time::days_from_now(0)?.as_ref())?; - x509_builder.set_not_after(Asn1Time::days_from_now(validity_days)?.as_ref())?; - - // Add extensions from the certificate extensions file - x509_builder.append_extension(BasicConstraints::new().critical().build()?)?; - x509_builder.append_extension( - KeyUsage::new() - .digital_signature() - .key_encipherment() - .build()?, - )?; - x509_builder.append_extension( - SubjectKeyIdentifier::new().build(&x509_builder.x509v3_context(None, None))?, - )?; - x509_builder.append_extension( - AuthorityKeyIdentifier::new() - .keyid(false) - .issuer(false) - .build(&x509_builder.x509v3_context(Some(ca_cert), None))?, - )?; - - x509_builder.sign(ca_private_key, MessageDigest::null())?; - Ok(x509_builder.build()) - } - - fn generate_ca_cert( - ca_private_key: &PKey, - cert_details: &PKIVaultCertDetails, - ) -> Result { - // Build the x509 name - let name = Self::build_x509_name( - &cert_details.ca.common_name, - &cert_details.country, - &cert_details.state, - &cert_details.locality, - &cert_details.organization, - &cert_details.org_unit, - )?; - - // Build the X.509 certificate - let mut x509_builder = X509Builder::new()?; - x509_builder.set_subject_name(&name)?; - x509_builder.set_issuer_name(&name)?; - x509_builder.set_pubkey(ca_private_key)?; - - // Set certificate validity period - x509_builder.set_not_before(Asn1Time::days_from_now(0)?.as_ref())?; - x509_builder - .set_not_after(Asn1Time::days_from_now(cert_details.ca.validity_days)?.as_ref())?; - - // Sign the certificate - x509_builder.sign(ca_private_key, MessageDigest::null())?; - Ok(x509_builder.build()) - } - - fn build_x509_name( - common_name: &str, - country: &str, - state: &str, - locality: &str, - organization: &str, - org_unit: &str, - ) -> Result { - let mut name_builder = X509NameBuilder::new()?; - name_builder.append_entry_by_text("C", country)?; - name_builder.append_entry_by_text("ST", state)?; - name_builder.append_entry_by_text("L", locality)?; - name_builder.append_entry_by_text("O", organization)?; - name_builder.append_entry_by_text("OU", org_unit)?; - name_builder.append_entry_by_text("CN", common_name)?; - let name = name_builder.build(); - - Ok(name) - } -} diff --git a/kbs/src/plugins/implementations/pki_vault/manager.rs b/kbs/src/plugins/implementations/pki_vault/manager.rs deleted file mode 100644 index d43400ccc9..0000000000 --- a/kbs/src/plugins/implementations/pki_vault/manager.rs +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright (c) 2025 by IBM Corporation -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use actix_web::http::Method; -use anyhow::{anyhow, bail, Context, Error, Result}; -use serde::Deserialize; -use std::sync::RwLock; -use std::{collections::HashMap, fs, path::PathBuf, sync::Arc}; - -use super::credential::{Credential, PKIVaultCertDetails}; -use crate::plugins::plugin_manager::ClientPlugin; - -const DEFAULT_PLUGIN_DIR: &str = "/opt/confidential-containers/kbs/plugin/pki_vault"; -const DEFAULT_CREDENTIALS_BLOB_FILE: &str = "certificates.json"; - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(default)] -pub struct PKIVaultPluginConfig { - pub plugin_dir: String, - pub cred_filename: String, - pub pkivault_cert_details: PKIVaultCertDetails, -} - -impl Default for PKIVaultPluginConfig { - fn default() -> Self { - PKIVaultPluginConfig { - plugin_dir: DEFAULT_PLUGIN_DIR.into(), - cred_filename: DEFAULT_CREDENTIALS_BLOB_FILE.into(), - pkivault_cert_details: PKIVaultCertDetails::default(), - } - } -} - -impl TryFrom for PKIVaultPlugin { - type Error = Error; - - fn try_from(config: PKIVaultPluginConfig) -> Result { - // Create the plugin dir if it does not exist - let plugin_dir = PathBuf::from(&config.plugin_dir); - if !plugin_dir.exists() { - fs::create_dir_all(&plugin_dir)?; - log::info!("plugin dir created = {}", plugin_dir.display()); - } - - // Read the existing credentials from file - let path = PathBuf::from(&config.plugin_dir) - .as_path() - .join(config.cred_filename); - - let credential: HashMap = if path.exists() { - match fs::read_to_string(&path) { - Ok(data) => serde_json::from_str(&data).unwrap_or_else(|_| HashMap::new()), - Err(_) => { - log::warn!("Error reading the credential file."); - HashMap::new() - } - } - } else { - log::warn!("Credentail file does not exist."); - HashMap::new() - }; - - // Initializing the PKI Vault plugin with existing credential data from file - Ok(PKIVaultPlugin { - plugin_dir: PathBuf::from(&config.plugin_dir), - cert_details: config.pkivault_cert_details, - credblob_file: path, - cred_store: Arc::new(RwLock::new(credential)), - }) - } -} - -/// Parameters for the credential request -/// -/// These parameters are provided in the request via URL query string. -/// Parameters taken by the "pki-vault" plugin to generate a unique key -/// for a sandbox store and retrieve credentials specific to the sandbox. -#[derive(Debug, PartialEq, serde::Deserialize)] -pub struct SandboxParams { - /// Required: ID of a sandbox or pod - pub id: String, - - /// Required: IP of a sandbox or pod - pub ip: String, - - /// Required: Name of a sandbox or pod - pub name: String, -} - -impl TryFrom<&str> for SandboxParams { - type Error = Error; - - fn try_from(query: &str) -> Result { - let params: SandboxParams = serde_qs::from_str(query)?; - Ok(params) - } -} - -/// Credentials necessary for initiating a server inside sandbox -#[derive(Debug, serde::Serialize)] -pub struct ServerCredential { - pub key: Vec, - pub cert: Vec, - pub ca_cert: Vec, -} - -/// Manages the credentials generation, handling requests -/// from backend, and credentials persistence storage -pub struct PKIVaultPlugin { - pub plugin_dir: PathBuf, - pub cert_details: PKIVaultCertDetails, - pub credblob_file: PathBuf, - pub cred_store: Arc>>, -} - -impl PKIVaultPlugin { - fn get_credential(&self, key: &str) -> Option { - let cred_store = self.cred_store.read().unwrap(); - cred_store.get(key).cloned() - } - - fn store_credential(&self, key: &str, credential: Credential) { - let mut cred_store = self.cred_store.write().unwrap(); - cred_store.insert(key.to_string(), credential); - } - - // Generate the credential (keys and certs for ca, server, and client) - fn generate_credential(&self, key: &str) -> Result> { - let credential = Credential::new(&self.cert_details)?; - - // Store the credential into the hashmap - self.store_credential(key, credential.clone()); - - // Write the hashmap to file for a persistence copy - if let Err(e) = self.save_hashmap(&self.credblob_file) { - log::warn!("Failed to store credentials into file: {}", e); - } - - log::info!("Returning newly generated credential!"); - let resource = ServerCredential { - key: credential.server_key.clone(), - cert: credential.server_cert.clone(), - ca_cert: credential.ca_cert.clone(), - }; - - Ok(serde_json::to_vec(&resource)?) - } - - fn save_hashmap(&self, path: &PathBuf) -> Result<()> { - let cred_store = self.cred_store.read().unwrap(); - let serialized = serde_json::to_string(&*cred_store)?; - fs::write(path, serialized)?; - Ok(()) - } - - async fn get_server_credential(&self, params: &SandboxParams) -> Result> { - // Return the server credential if the credential presents in the hashmap - let key = format!("{}_{}_{}", ¶ms.name, ¶ms.ip, ¶ms.id); - if let Some(credential) = self.get_credential(&key) { - log::info!("Returning existing credential!"); - - let resource = ServerCredential { - key: credential.server_key, - cert: credential.server_cert, - ca_cert: credential.ca_cert, - }; - - return Ok(serde_json::to_vec(&resource)?); - }; - - // Otherwise return newly generated credential - self.generate_credential(&key) - } -} - -#[async_trait::async_trait] -impl ClientPlugin for PKIVaultPlugin { - async fn handle( - &self, - _body: &[u8], - query: &str, - path: &str, - method: &Method, - ) -> Result> { - let sub_path = path - .strip_prefix('/') - .context("accessed path is illegal, should start with `/`")?; - if method.as_str() != "GET" { - bail!("Illegal HTTP method. Only GET is supported"); - } - - match sub_path { - "credential" => { - let params = SandboxParams::try_from(query)?; - let credential = self.get_server_credential(¶ms).await?; - - Ok(credential) - } - _ => Err(anyhow!("{} not supported", sub_path))?, - } - } - - async fn validate_auth( - &self, - _body: &[u8], - _query: &str, - _path: &str, - _method: &Method, - ) -> Result { - Ok(false) - } - - /// Whether the body needs to be encrypted via TEE key pair. - /// If returns `Ok(true)`, the KBS server will encrypt the whole body - /// with TEE key pair and use KBS protocol's Response format. - async fn encrypted( - &self, - _body: &[u8], - _query: &str, - _path: &str, - _method: &Method, - ) -> Result { - Ok(true) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tokio; - - #[tokio::test] - async fn test_handle() { - let config = PKIVaultPluginConfig::default(); - let plugin = PKIVaultPlugin::try_from(config).unwrap(); - - // Define sample inputs - let body: &[u8] = b""; - let query = "id=3367348&ip=60.11.12.43&name=pod7"; - let path = "/credential"; - let method = &Method::GET; - - // Act: call the handle method - let result = plugin.handle(body, query, path, method).await; - - // Assert: check the result - match result { - Ok(response) => { - // Expected results - let key = String::from("pod7_60.11.12.43_3367348"); - - if let Some(credential) = plugin.get_credential(&key) { - let resource = ServerCredential { - key: credential.server_key, - cert: credential.server_cert, - ca_cert: credential.ca_cert, - }; - - let expected_response = serde_json::to_vec(&resource).unwrap(); - assert_eq!(response, expected_response); - }; - } - Err(e) => panic!("Expected Ok, got Err: {:?}", e), - } - } -} diff --git a/kbs/src/plugins/implementations/pki_vault/mod.rs b/kbs/src/plugins/implementations/pki_vault/mod.rs deleted file mode 100644 index d4bef1d010..0000000000 --- a/kbs/src/plugins/implementations/pki_vault/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod credential; -pub mod manager; - -pub use manager::{PKIVaultPlugin, PKIVaultPluginConfig}; diff --git a/kbs/src/plugins/plugin_manager.rs b/kbs/src/plugins/plugin_manager.rs index b18a57f7cc..b1836ac375 100644 --- a/kbs/src/plugins/plugin_manager.rs +++ b/kbs/src/plugins/plugin_manager.rs @@ -13,12 +13,12 @@ use super::{sample, RepositoryConfig, ResourceStorage}; #[cfg(feature = "nebula-ca-plugin")] use super::{NebulaCaPlugin, NebulaCaPluginConfig}; -#[cfg(feature = "pki-vault-plugin")] -use super::{PKIVaultPlugin, PKIVaultPluginConfig}; - #[cfg(feature = "pkcs11")] use super::{Pkcs11Backend, Pkcs11Config}; +#[cfg(feature = "pki-vault-plugin")] +use super::{PKIVaultPlugin, PKIVaultPluginConfig}; + type ClientPluginInstance = Arc; #[async_trait::async_trait] @@ -91,8 +91,6 @@ impl Display for PluginsConfig { PluginsConfig::NebulaCaPlugin(_) => f.write_str("nebula-ca"), #[cfg(feature = "pkcs11")] PluginsConfig::Pkcs11(_) => f.write_str("pkcs11"), - #[cfg(feature = "splitapi-plugin")] - PluginsConfig::SplitAPI(_) => f.write_str("splitapi"), #[cfg(feature = "pki-vault-plugin")] PluginsConfig::PKIVaultPlugin(_) => f.write_str("pki_vault"), } From 6bb1ad99dabe50c858e1fa0a3df1bd04a944ff4e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 11 Nov 2025 20:58:07 -0500 Subject: [PATCH 5/5] Remove stateful credetentials support Changes include the following functionalities: - refactor the code for on-demand generation of credentials - remove the stateful credentials support from the initial verison - update the readme documentation - fix comments Signed-off-by: Salman Ahmed --- kbs/docs/pki_vault.md | 186 ------------- kbs/docs/plugins/pki_vault.md | 147 ++++++++++ kbs/src/plugins/implementations/pki_vault.rs | 277 ++++++++----------- 3 files changed, 269 insertions(+), 341 deletions(-) delete mode 100644 kbs/docs/pki_vault.md create mode 100644 kbs/docs/plugins/pki_vault.md diff --git a/kbs/docs/pki_vault.md b/kbs/docs/pki_vault.md deleted file mode 100644 index 0914023eb8..0000000000 --- a/kbs/docs/pki_vault.md +++ /dev/null @@ -1,186 +0,0 @@ -# PKI Vault plugin - -This plugin currently generates credentials (keys and certificates) for a server running inside the confidential VM (aka sandbox) and for the workload owner (who acts as a client for the server). The current design of the plugin prioritizes the mutual authentication between the server and the client. Such design is necessary for the SplitAPI (kata-containers/kata-containers#9159 and -kata-containers/kata-containers#9752) and peer-pods. - -This plugin also delivers the server-specific credentials to the sandbox (i.e., confidential PODs or VMs), specifically to the kata agent to initiate the server. The workload owner can communicate with the server using a secure tunnel. - -The server-specific credentials can be obtained throught the `get-resource` APIs by specifying the `plugin-name`. Currently, the plugin requires that the sandbox or `kata-agent` sends the IPv4 address, name, and the ID of the sandbox (i.e., pod) as part of the query string to obtain the credentials from the KBS. - -After receiving the credential request, the `pki_vault` plugin will create a CA, a key pair for the server and another key pair for client, and sign them using the self-signed CA. Currently, the generated credentials are stored in a hashmap with a unique key for each sandbox based on its name, ID, and IP address. But we expect this design will be changed in future. PKI Vault plugin responds to a request from the sandbox by sending the server specific credentials (key, cert) along with the CA certificate. A request from workload owner gets the client specific credentials (key, cert) and CA's certificate. - -# Setup - -1. Build the KBS with the cargo feature `pki-vault-plugin` enabled. - -```bash -make background-check-kbs POLICY_ENGINE=opa PKI_VAULT_PLUGIN=true -``` - -2. Configure the `pki-vault` plugin. Simply specifying the plugin name should be enough for the configuration, just add the lines below to the [KBS config](#kbs/config/docker-compose/kbs-config.toml). But one can specify addition details to the configuration file. - -```toml -[[plugins]] -name = "pki_vault" -``` -The following addition details can be set to the configuration file. -```toml -[[plugins]] -name = "pki_vault" -#plugin_dir = "/opt/confidential-containers/kbs/plugin/splitapi" -#cred_filename = "certificates.json" -[plugins.pkivault_cert_details] -country = "AA" -#state = "Default State" -#locality = "Default City" -organization = "Default Organization" -org_unit = "Default Unit" - -[plugins.pkivault_cert_details.ca] -#common_name = "grpc-tls CA" -validity_days = 3650 - -[plugins.pkivault_cert_details.server] -#common_name = "server" -#validity_days = 180 - -[plugins.pkivault_cert_details.client] -common_name = "client" -validity_days = 180 -``` - -3. Start trustee - -```bash -sudo ../target/release/kbs --config-file ./config/kbs-config.toml -``` -# Design choices [To be updated] - -## 1. Separate CA for each server (sandbox) -## 2. Single CA for all -## 3. Persistence vs. non-persistence - -# Runtime services - -All runtime services for both the server and client supported are described in the following sections. - -## Credentials service for server (i.e., pod or sandbox) - -Request the credentials for initiating a server inside a pod or sandbox. - -Only `GET` request is supported, e.g. `GET /kbs/v0/pki_vault/credentials?id=3367&ip=60.11.12.89&name=pod51`. Current the `GET` takes `id`, `ip`, and `name` parameters, but expect this parameters to be changed in the future design for supporting more generic use cases. - -The request takes parameters via URL query string. All parameters supported are described in the table below. Note currently, all parameters are required. - -| Property | Type | Required | Description | Default | Example | -|---------------------|--------|----------|-------------------------|---------|-------------------------------------------| -| `name` | String | Yes | Name of the pod | | `credentials?id=3367&ip=60.11.12.89&name=pod51` | -| `ip` | String | Yes | IPv4 address of a pod to assign to the certificate | | `credentials?id=3367&ip=60.11.12.89&name=pod51` | -| `id` | String | Yes | Pod ID | | `credentials?id=3367&ip=60.11.12.89&name=pod51` | - -The request will be processed only if the node passes the attestation, otherwise an error is returned. If the credentials for the server already already exists in the KBS, the plugin simply returns the existing credentials. Otherwise, the plugin generates the credentials. - -Once the request is processed, the following structure is returned in JSON format. - -```rust -pub struct CredentialsOut { - pub key: Vec, // Key created - pub cert: Vec, // Self-signed certificate created - pub ca_cert: Vec, // CA certificate -} -``` - - - -## Credentials services for client (i.e., workload owner) - -Request the client credentials for initiating a mutual TLS communication channel between client (workload owner) and the server (running inside a pod or sandbox). - -### `list_pods` -Only `GET` request is supported, e.g. `GET /kbs/v0/pki_vault/list_pods`. Here, `list_pods` is an API that the client (or workload owner) can invoke to get the list of pod names and their additional information. - -### `get_client_credentials` -Only `GET` request is supported, e.g. `GET /kbs/v0/pki_vault/get_client_credentials?id=3367&ip=60.11.12.89&name=pod51`. Here, `get_client_credentials` is an API that the client (or workload owner) can invoke to get the owner or client-specific credentails for a pod. A pod obtains the server-specific credentials through the `get_resource` API. The `get_client_credentials` API need a `query` parameter to indicate the target pod. - -All APIs supported are described in the table below. - -| API | Description | Example | -|---------------------|-------------------------|-------------------------------------------| -| `list_pods` | To get the list of pods that have server credentials created| `kbs/v0/pki_vault/list_pods` | -| `get_client_credentials` | To get the client credentials for a pod | `/kbs/v0/pki_vault/get_client_credentials?id=3367&ip=60.11.12.89&name=pod51` | - -The request will be processed only if the request is authenticated, otherwise an error is returned. In the current design, the credentials for the client already already exists in the KBS as they have been already created as part of the response of server credential request, so the plugin simply returns the existing client credentials. - -Once the request is processed, the following structure is returned in JSON format. - -```rust -pub struct CredentialsOut { - pub key: Vec, // Key created - pub cert: Vec, // Self-signed certificate created - pub ca_cert: Vec, // CA certificate -} -``` - - -# How to test (client or owner-side code) -In order to test the `pki-vault` plugin, we need to enable the plugin support in the `kbs_protocol` inside `attestation-agent` of the `guest-components` respository. The plugin support has been enabled in the `pki-vault-test` branch of following `guest-components` repository. - -`https://github.com/salmanyam/guest-components/tree/pki-vault-test` - -In addition to that, we need to patch the `kbs-client` located inside `tools` of `trustee`, so that the `kbs-client` can invoke the plugin call. A patched version of the `kbs-client` can be located in the `pki-vault-test` branch of the following `trustee` repository. - -`https://github.com/salmanyam/trustee/commits/pki-vault-test/` - -This patched `kbs-client` has also used the patched `guest-component` with the modified `kbs_protocol`. - -``` -kbs_protocol = { git = "https://github.com/salmanyam/guest-components.git", rev = "281be58d49e91a13a1c55fb30324c705f9d0d9c5", default-features = false } -kms = { git = "https://github.com/salmanyam/guest-components.git", rev = "281be58d49e91a13a1c55fb30324c705f9d0d9c5", default-features = false } -``` - -Once the `kbs-client` is built, use the `kbs-client` to make a request to KBS for generating and providing the server specific credentials. - -## Step-by-step instructions - -Pull the patched `kbs-client` code -``` -$ git clone https://github.com/salmanyam/trustee.git -$ cd trustee -$ git checkout pki-vault-test -``` - -Build the `kbs-client` code -``` -$ cd kbs && make cli -``` -> **Note:** To specify an attester, please use the `ATTESTER` environment variable, e.g., `ATTESTER=snp-attester` - - - -To invoke the `get_resource` API. KBS should return the server specific credentials such as server key, server certificates, and the certificate of the CA. -``` -$ sudo ../target/release/kbs-client --url http://127.0.0.1:8080 get-resource --plugin-name "pki_vault" --resource-path "credentials?id=3367353&ip=60.11.12.48&name=pod33" | base64 -d -``` - - -To invoke the `list_pods` API. This should return the list of pod names and associated information. - -``` -$ sudo ../target/release/kbs-client --url http://127.0.0.1:8080 pki-vault-config --auth-private-key config/private.key list-pods -``` - - - -To invoke the `get_client_credentials` API. This should return the client credentials of the following format. -``` -$ sudo ../target/release/kbs-client --url http://127.0.0.1:8080 pki-vault-config --auth-private-key config/private.key get-client-credentials --query "name=pod51&ip=60.11.12.89&id=3367383" -``` - - -``` -pub struct Credentials { - pub key: Vec, - pub cert: Vec, - pub ca_cert: Vec, -} -``` diff --git a/kbs/docs/plugins/pki_vault.md b/kbs/docs/plugins/pki_vault.md new file mode 100644 index 0000000000..c41e439660 --- /dev/null +++ b/kbs/docs/plugins/pki_vault.md @@ -0,0 +1,147 @@ +# PKI Vault plugin + +This plugin currently generates credentials (keys and certificates) for the confidential VM (aka pod VM or sandbox) and for the owner of the confidential workload that runs inside the confidential VM as a confidential container. The credentials allow us to establish a secured client-server communication where the owner acts as a client for the server. Such a design is useful for the SplitAPI (kata-containers/kata-containers#9159 and +kata-containers/kata-containers#9752) and peer-pods. The current design of the plugin prioritizes the mutual authentication between the server and the client. + +This plugin also delivers the credentials to the confidential pod VM through the invocation of the `get-resource` API call by specifying the `plugin-name`. Currently, the plugin requires that the pod VM sends an identifier (required) and any additinal parameters as part of the query string passed to the `get-resource` API as the `resource-path`. The identifier is used as a key to obtain the credentials from the KBS. + +Once receiving the credential request from pod VM through `get-resource` API, the plugin creates a CA and two key pairs (one for pod VM and another for workload owner), and signs the key pairs using the self-signed CA. Currently, the generated credentials are stored in a hashmap with the identifer as the key for each pod VM. PKI Vault plugin responds to a request from the pod VM by sending the server specific credentials (key, cert) along with the CA certificate. A request from workload owner gets the client specific credentials (key, cert) and CA's certificate. + +# Setup + +1. Build the KBS with the cargo feature `pki-vault-plugin` enabled. + +```bash +make background-check-kbs POLICY_ENGINE=opa PKI_VAULT_PLUGIN=true +``` + +2. Configure the `pki-vault` plugin. Simply specifying the plugin name should be enough for the configuration, just add the lines below to the [KBS config](#kbs/config/docker-compose/kbs-config.toml). But one can specify addition details to the configuration file. + +```toml +[[plugins]] +name = "pki_vault" +``` +The following addition details can be set to the configuration file. +```toml +[[plugins]] +name = "pki_vault" +plugin_dir = "/opt/confidential-containers/kbs/plugin/splitapi" +cred_filename = "certificates.json" + +[plugins.pkivault_cert_details] +country = "AA" +state = "Default State" +locality = "Default City" +organization = "Default Organization" +org_unit = "Default Unit" + +[plugins.pkivault_cert_details.ca] +common_name = "grpc-tls CA" +validity_days = 3650 + +[plugins.pkivault_cert_details.server] +common_name = "server" +validity_days = 180 + +[plugins.pkivault_cert_details.client] +common_name = "client" +validity_days = 180 +``` + +3. Start trustee + +```bash +sudo ../target/release/kbs --config-file ./config/kbs-config.toml +``` +# Design choices +There can be three key design choices one can consider to configure PKI Vault plugin. As of now, PKI Vault only supports the separte CA per Pod approach with a non-persistent credential storage. + +1. **Separate CA per Pod VM**: +Each pod VM gets its own Certificate Authority (CA), meaning every sandbox has an independent root of trust. This design isolates security domains — if one pod’s CA is compromised, others remain safe. It also simplifies per-pod credential generation and teardown. However, it creates more CA certificates to manage and requires persistent storage so that a pod’s CA isn’t lost after a restart. + +2. **Single CA for All Pod VMs**: +Using one global CA to sign credentials for all pod VMs simplifies trust management, since all pods share the same root certificate. It reduces operational overhead and avoids losing trust links after service restarts. The downside is that it introduces a single point of failure — if the CA key is compromised, all pods are affected — and reduces isolation between sandboxes. + +3. **Persistent vs. Non-Persistent Credential Storage**: +Persistent storage keeps CA keys and certificates on disk so they survive restarts, maintaining continuity in authentication. Non-persistent storage is simpler and more secure in the sense that credentials vanish after shutdown, but it breaks mutual authentication if the service restarts. In this design, persistence is chosen to ensure pod CAs and credentials remain valid even after trustee restarts. + +# Runtime services + +All the supported runtime services for both the Pod VM and workload owner are described in the following sections. + +## Credentials service for Pod VM + +A Pod VM requests the the credentials (e.g., to initiate a server inside the pod VM) through the `GET` request. + +Only the `GET` request is supported, e.g., `GET /kbs/v0/pki_vault/credentials?token=podToken12345&name=pod51&ip=60.11.12.89`. Currently, the `GET` takes `name`, `token`, and `ip` parameters. The Pod VM can pass the `name` and `ip` parameters to the request, but owner has to set the `token` to the Pod VM through init data. The `token`, together with the `name` and `ip`, serves as a unique identifier for associating a Pod VM with its record. This identifier is used as the key in a hashmap to store the Pod VM’s credentials. + +The request takes parameters via URL query string. All parameters supported are described in the table below. + +>**Note:** Currently, all parameters are required. We have plan to support additional parameters in the query string in later version. A policy can help check the query string. + +| Property | Type | Required | Description | Example | +|---------------------|--------|----------|-------------------------|-------------------------------------------| +| `token` | String | Yes | Token assigned to the Pod by owner | `credentials?token=podToken12345` | +| `name` | String | Yes | Pod name | `credentials?name=pod51` | +| `ip` | String | Yes | IPv4 address of a pod to assign to the certificate | `credentials?ip=60.11.12.89` | + + +The request will be processed only if the node passes the attestation, otherwise an error is returned. If the credentials for the Pod VM already exists in Trustee, the plugin simply returns the existing credentials. Otherwise, the plugin generates the credentials. + +Once the request is processed, the following structure is returned in JSON format. + +```rust +pub struct CredentialsOut { + pub key: Vec, // Key created + pub cert: Vec, // Self-signed certificate created + pub ca_cert: Vec, // CA certificate +} +``` +Example credentials are below: +```rust +{ + "key":[45,45,45,45,45,66,69,71,73,78,32,80,82,73,86,65,84,69,32,75,69,89,45,45,45,45,45,10,77,67,52,67,65,81,65,119,66,81,89,68,75,50,86,119,66,67,73,69,73,79,109,112,47,49,69,73,50,82,65,90,73,99,117,87,99,108,54,80,111,100,104,101,79,85,68,119,65,57,52,82,109,109,78,83,81,114,86,98,50,50,113,55,10,45,45,45,45,45,69,78,68,32,80,82,73,86,65,84,69,32,75,69,89,45,45,45,45,45,10], + "cert":[45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,110,68,67,67,65,107,54,103,65,119,73,66,65,103,73,66,65,106,65,70,66,103,77,114,90,88,65,119,103,89,103,120,67,122,65,74,66,103,78,86,66,65,89,84,65,107,70,66,77,82,89,119,70,65,89,68,86,81,81,73,10,68,65,49,69,90,87,90,104,100,87,120,48,73,70,78,48,89,88,82,108,77,82,85,119,69,119,89,68,86,81,81,72,68,65,120,69,90,87,90,104,100,87,120,48,73,69,78,112,100,72,107,120,72,84,65,98,66,103,78,86,66,65,111,77,10,70,69,82,108,90,109,70,49,98,72,81,103,84,51,74,110,89,87,53,112,101,109,70,48,97,87,57,117,77,82,85,119,69,119,89,68,86,81,81,76,68,65,120,69,90,87,90,104,100,87,120,48,73,70,86,117,97,88,81,120,70,68,65,83,10,66,103,78,86,66,65,77,77,67,50,100,121,99,71,77,116,100,71,120,122,73,69,78,66,77,66,52,88,68,84,73,49,77,84,69,120,77,84,69,50,77,122,81,121,78,49,111,88,68,84,73,50,77,68,85,120,77,68,69,50,77,122,81,121,10,78,49,111,119,103,89,77,120,67,122,65,74,66,103,78,86,66,65,89,84,65,107,70,66,77,82,89,119,70,65,89,68,86,81,81,73,68,65,49,69,90,87,90,104,100,87,120,48,73,70,78,48,89,88,82,108,77,82,85,119,69,119,89,68,10,86,81,81,72,68,65,120,69,90,87,90,104,100,87,120,48,73,69,78,112,100,72,107,120,72,84,65,98,66,103,78,86,66,65,111,77,70,69,82,108,90,109,70,49,98,72,81,103,84,51,74,110,89,87,53,112,101,109,70,48,97,87,57,117,10,77,82,85,119,69,119,89,68,86,81,81,76,68,65,120,69,90,87,90,104,100,87,120,48,73,70,86,117,97,88,81,120,68,122,65,78,66,103,78,86,66,65,77,77,66,110,78,108,99,110,90,108,99,106,65,113,77,65,85,71,65,121,116,108,10,99,65,77,104,65,80,122,80,102,119,105,57,69,51,48,47,88,114,117,99,88,66,88,119,97,120,68,118,109,108,102,47,122,77,57,88,115,51,56,53,84,72,69,104,72,110,111,76,111,52,72,102,77,73,72,99,77,65,119,71,65,49,85,100,10,69,119,69,66,47,119,81,67,77,65,65,119,67,119,89,68,86,82,48,80,66,65,81,68,65,103,87,103,77,66,48,71,65,49,85,100,68,103,81,87,66,66,84,66,69,75,99,69,89,65,122,67,57,88,85,72,51,105,43,98,71,113,101,68,10,84,74,54,79,97,84,67,66,110,119,89,68,86,82,48,106,66,73,71,88,77,73,71,85,111,89,71,79,112,73,71,76,77,73,71,73,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,10,67,65,119,78,82,71,86,109,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,10,68,66,82,69,90,87,90,104,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,10,69,103,89,68,86,81,81,68,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,89,73,66,65,68,65,70,66,103,77,114,90,88,65,68,81,81,67,104,54,98,66,117,77,53,65,79,119,52,80,116,117,109,90,49,54,111,55,83,10,103,112,99,104,121,90,99,51,102,71,97,78,78,51,43,65,53,47,111,81,99,98,70,48,108,73,108,106,75,88,121,56,121,90,90,113,51,119,89,87,78,116,99,115,122,48,54,118,102,116,117,99,86,88,105,66,47,103,109,120,82,86,85,77,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10], + "ca_cert":[45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,66,117,106,67,67,65,87,119,67,65,81,65,119,66,81,89,68,75,50,86,119,77,73,71,73,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,67,65,119,78,82,71,86,109,10,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,68,66,82,69,90,87,90,104,10,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,69,103,89,68,86,81,81,68,10,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,84,65,101,70,119,48,121,78,84,69,120,77,84,69,120,78,106,77,48,77,106,100,97,70,119,48,122,78,84,69,120,77,68,107,120,78,106,77,48,77,106,100,97,77,73,71,73,10,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,67,65,119,78,82,71,86,109,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,10,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,68,66,82,69,90,87,90,104,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,10,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,69,103,89,68,86,81,81,68,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,84,65,113,77,65,85,71,65,121,116,108,10,99,65,77,104,65,74,116,89,115,52,68,83,115,51,81,53,117,78,54,75,51,55,77,103,51,50,76,104,88,103,100,89,111,49,69,55,101,104,120,70,82,100,119,65,102,122,57,71,77,65,85,71,65,121,116,108,99,65,78,66,65,79,73,103,10,90,105,109,66,47,101,120,106,77,47,78,113,117,52,106,65,86,116,68,74,47,107,108,109,84,97,103,76,79,98,109,103,122,107,114,110,55,50,102,53,69,51,84,104,121,116,69,49,79,85,104,83,114,109,102,98,97,118,83,114,78,57,73,90,10,102,119,57,98,66,81,110,79,101,87,78,113,122,109,101,74,115,119,52,61,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10] +} +``` + +## Credentials services for workload owner + +The workload owners can retrieve the client (or owner) specific credentials from Trustee. A workload woner (who has the private key of Trustee) can invoke the following APIs. + +### `list_pods` +Only `GET` request is supported, e.g. `GET /kbs/v0/pki_vault/list_pods`. Here, `list_pods` is an API that the client (or workload owner) can invoke to get the list of pod names and their additional information. + +### `client_credentials` +Only `GET` request is supported, e.g. `GET /kbs/v0/pki_vault/client_credentials?token=podToken12345&name=pod51&ip=60.11.12.89`. Here, `client_credentials` is an API that the workload owner can invoke to get the owner or client-specific credentails for a pod. A Pod VM obtains the server-specific credentials through the `get_resource` API. The `client_credentials` API also need to pass the same set of parameters through the `query` string to indicate the target pod. + +All APIs supported are described in the table below. + +| API | Description | Example | +|---------------------|-------------------------|-------------------------------------------| +| `list_pods` | To get the list of pods that have server credentials created| `kbs/v0/pki_vault/list_pods` | +| `client_credentials` | To get the client credentials for a pod | `/kbs/v0/pki_vault/client_credentials?token=podToken12345&name=pod51&ip=60.11.12.89` | + +The request will be processed only if the request is authenticated, otherwise an error is returned. The credentials for the client already already exists in the KBS as they have been already created as part of the response of server credential request, so the plugin simply returns the existing client credentials. + +Once the request is processed, the following structure is returned in JSON format. + +```rust +pub struct CredentialsOut { + pub key: Vec, // Key created + pub cert: Vec, // Self-signed certificate created + pub ca_cert: Vec, // CA certificate +} +``` + +Example credentials are below: + +```rust +{ + key:[45,45,45,45,45,66,69,71,73,78,32,80,82,73,86,65,84,69,32,75,69,89,45,45,45,45,45,10,77,67,52,67,65,81,65,119,66,81,89,68,75,50,86,119,66,67,73,69,73,66,114,83,49,51,47,79,56,65,76,53,108,116,122,117,105,78,105,53,82,120,48,82,56,57,90,105,116,49,103,88,67,77,88,89,51,80,47,87,81,56,86,97,10,45,45,45,45,45,69,78,68,32,80,82,73,86,65,84,69,32,75,69,89,45,45,45,45,45,10], + cert:[45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,110,68,67,67,65,107,54,103,65,119,73,66,65,103,73,66,65,106,65,70,66,103,77,114,90,88,65,119,103,89,103,120,67,122,65,74,66,103,78,86,66,65,89,84,65,107,70,66,77,82,89,119,70,65,89,68,86,81,81,73,10,68,65,49,69,90,87,90,104,100,87,120,48,73,70,78,48,89,88,82,108,77,82,85,119,69,119,89,68,86,81,81,72,68,65,120,69,90,87,90,104,100,87,120,48,73,69,78,112,100,72,107,120,72,84,65,98,66,103,78,86,66,65,111,77,10,70,69,82,108,90,109,70,49,98,72,81,103,84,51,74,110,89,87,53,112,101,109,70,48,97,87,57,117,77,82,85,119,69,119,89,68,86,81,81,76,68,65,120,69,90,87,90,104,100,87,120,48,73,70,86,117,97,88,81,120,70,68,65,83,10,66,103,78,86,66,65,77,77,67,50,100,121,99,71,77,116,100,71,120,122,73,69,78,66,77,66,52,88,68,84,73,49,77,84,69,120,77,84,69,50,78,68,89,121,78,49,111,88,68,84,73,50,77,68,85,120,77,68,69,50,78,68,89,121,10,78,49,111,119,103,89,77,120,67,122,65,74,66,103,78,86,66,65,89,84,65,107,70,66,77,82,89,119,70,65,89,68,86,81,81,73,68,65,49,69,90,87,90,104,100,87,120,48,73,70,78,48,89,88,82,108,77,82,85,119,69,119,89,68,10,86,81,81,72,68,65,120,69,90,87,90,104,100,87,120,48,73,69,78,112,100,72,107,120,72,84,65,98,66,103,78,86,66,65,111,77,70,69,82,108,90,109,70,49,98,72,81,103,84,51,74,110,89,87,53,112,101,109,70,48,97,87,57,117,10,77,82,85,119,69,119,89,68,86,81,81,76,68,65,120,69,90,87,90,104,100,87,120,48,73,70,86,117,97,88,81,120,68,122,65,78,66,103,78,86,66,65,77,77,66,110,78,108,99,110,90,108,99,106,65,113,77,65,85,71,65,121,116,108,10,99,65,77,104,65,66,88,105,90,70,54,106,115,102,48,72,53,88,121,43,66,117,107,109,65,65,115,90,81,51,85,66,89,108,86,121,119,66,103,82,88,72,71,77,54,102,103,56,111,52,72,102,77,73,72,99,77,65,119,71,65,49,85,100,10,69,119,69,66,47,119,81,67,77,65,65,119,67,119,89,68,86,82,48,80,66,65,81,68,65,103,87,103,77,66,48,71,65,49,85,100,68,103,81,87,66,66,81,68,108,55,70,52,85,90,106,104,71,55,80,90,50,43,105,52,70,70,74,75,10,104,114,72,77,55,122,67,66,110,119,89,68,86,82,48,106,66,73,71,88,77,73,71,85,111,89,71,79,112,73,71,76,77,73,71,73,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,10,67,65,119,78,82,71,86,109,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,10,68,66,82,69,90,87,90,104,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,10,69,103,89,68,86,81,81,68,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,89,73,66,65,68,65,70,66,103,77,114,90,88,65,68,81,81,65,54,48,80,122,65,67,74,121,87,109,77,104,67,43,71,57,98,118,102,67,80,10,116,66,102,109,86,67,89,89,90,116,108,84,115,90,99,104,51,112,118,116,76,102,81,114,55,113,105,79,111,112,71,57,87,50,87,49,104,75,77,50,87,103,105,104,51,114,108,106,67,80,115,70,83,70,90,115,86,99,85,119,118,56,69,71,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10], + ca_cert:[45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,66,117,106,67,67,65,87,119,67,65,81,65,119,66,81,89,68,75,50,86,119,77,73,71,73,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,67,65,119,78,82,71,86,109,10,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,68,66,82,69,90,87,90,104,10,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,69,103,89,68,86,81,81,68,10,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,84,65,101,70,119,48,121,78,84,69,120,77,84,69,120,78,106,77,48,77,106,100,97,70,119,48,122,78,84,69,120,77,68,107,120,78,106,77,48,77,106,100,97,77,73,71,73,10,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,67,65,119,78,82,71,86,109,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,10,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,68,66,82,69,90,87,90,104,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,10,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,69,103,89,68,86,81,81,68,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,84,65,113,77,65,85,71,65,121,116,108,10,99,65,77,104,65,74,116,89,115,52,68,83,115,51,81,53,117,78,54,75,51,55,77,103,51,50,76,104,88,103,100,89,111,49,69,55,101,104,120,70,82,100,119,65,102,122,57,71,77,65,85,71,65,121,116,108,99,65,78,66,65,79,73,103,10,90,105,109,66,47,101,120,106,77,47,78,113,117,52,106,65,86,116,68,74,47,107,108,109,84,97,103,76,79,98,109,103,122,107,114,110,55,50,102,53,69,51,84,104,121,116,69,49,79,85,104,83,114,109,102,98,97,118,83,114,78,57,73,90,10,102,119,57,98,66,81,110,79,101,87,78,113,122,109,101,74,115,119,52,61,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10] + +} +``` \ No newline at end of file diff --git a/kbs/src/plugins/implementations/pki_vault.rs b/kbs/src/plugins/implementations/pki_vault.rs index 338d8a5971..8b52f7a47c 100644 --- a/kbs/src/plugins/implementations/pki_vault.rs +++ b/kbs/src/plugins/implementations/pki_vault.rs @@ -5,7 +5,7 @@ use actix_web::http::Method; use anyhow::{anyhow, bail, Context, Error, Result}; use std::sync::RwLock; -use std::{collections::HashMap, fs, path::PathBuf, sync::Arc}; +use std::{collections::HashMap, sync::Arc}; use openssl::asn1::Asn1Time; use openssl::bn::BigNum; @@ -19,9 +19,6 @@ use serde::{Deserialize, Serialize}; use crate::plugins::plugin_manager::ClientPlugin; -const DEFAULT_PLUGIN_DIR: &str = "/opt/confidential-containers/kbs/plugin/pki_vault"; -const DEFAULT_CREDENTIALS_BLOB_FILE: &str = "certificates.json"; - /// Default certificate details if not configured pub const DEFAULT_COUNTRY: &str = "AA"; pub const DEFAULT_STATE: &str = "Default State"; @@ -121,49 +118,52 @@ impl Default for PKIVaultCertDetails { /// Credentials necessary for mutual TLS communication #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Credentials { - pub ca_cert: Vec, - pub server_key: Vec, - pub server_cert: Vec, - pub client_key: Vec, - pub client_cert: Vec, +pub struct PKIVaultCA { + pub key: Vec, + pub cert: Vec, } -impl Credentials { +impl PKIVaultCA { pub fn new(cert_details: &PKIVaultCertDetails) -> Result { // Private keys for CA, Server, and Client - let ca_private_key = PKey::generate_ed25519()?; - let server_private_key = PKey::generate_ed25519()?; - let client_private_key = PKey::generate_ed25519()?; + let key = PKey::generate_ed25519()?; // Generate CA certificate - let ca_cert = Self::generate_ca_cert(&ca_private_key, cert_details)?; + let cert = Self::generate_ca_cert(&key, cert_details)?; - // Generate certificate for Server - let server_cert = Self::generate_signed_cert( - &server_private_key, - &ca_cert, - &ca_private_key, - cert_details, - "server", - )?; + Ok(Self { + key: key.private_key_to_pem_pkcs8()?, + cert: cert.to_pem()?, + }) + } - // Generate certificate for Client - let client_cert = Self::generate_signed_cert( - &client_private_key, + /// Initializes a PKIVaultCA from existing key and certificate bytes. + pub fn init(key: Vec, cert: Vec) -> Result { + // Optional: Validate that the key and cert are valid before returning + let _ = PKey::private_key_from_pem(&key)?; + let _ = X509::from_pem(&cert)?; + + Ok(Self { key, cert }) + } + + fn generate_credentials( + &self, + ca_cert: &X509, + ca_private_key: &PKey, + cert_details: &PKIVaultCertDetails, + for_podvm_or_owner: &str, + ) -> Result<(PKey, X509)> { + // Generate private key and certificate + let key = PKey::generate_ed25519()?; + let cert = Self::generate_signed_cert( + &key, &ca_cert, &ca_private_key, cert_details, - "client", + for_podvm_or_owner, )?; - Ok(Self { - ca_cert: ca_cert.to_pem()?, - server_key: server_private_key.private_key_to_pem_pkcs8()?, - server_cert: server_cert.to_pem()?, - client_key: client_private_key.private_key_to_pem_pkcs8()?, - client_cert: client_cert.to_pem()?, - }) + Ok((key, cert)) } fn generate_signed_cert( @@ -280,20 +280,15 @@ impl Credentials { } } - #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(default)] pub struct PKIVaultPluginConfig { - pub plugin_dir: String, - pub cred_filename: String, pub pkivault_cert_details: PKIVaultCertDetails, } impl Default for PKIVaultPluginConfig { fn default() -> Self { PKIVaultPluginConfig { - plugin_dir: DEFAULT_PLUGIN_DIR.into(), - cred_filename: DEFAULT_CREDENTIALS_BLOB_FILE.into(), pkivault_cert_details: PKIVaultCertDetails::default(), } } @@ -303,37 +298,12 @@ impl TryFrom for PKIVaultPlugin { type Error = Error; fn try_from(config: PKIVaultPluginConfig) -> Result { - // Create the plugin dir if it does not exist - let plugin_dir = PathBuf::from(&config.plugin_dir); - if !plugin_dir.exists() { - fs::create_dir_all(&plugin_dir)?; - log::info!("plugin dir created = {}", plugin_dir.display()); - } - - // Read the existing credentials from file - let path = PathBuf::from(&config.plugin_dir) - .as_path() - .join(config.cred_filename); - - let credential: HashMap = if path.exists() { - match fs::read_to_string(&path) { - Ok(data) => serde_json::from_str(&data).unwrap_or_else(|_| HashMap::new()), - Err(_) => { - log::warn!("Error reading the credentials file."); - HashMap::new() - } - } - } else { - log::warn!("Credentails file does not exist."); - HashMap::new() - }; + let empty_cas: HashMap = HashMap::new(); // Initializing the PKI Vault plugin with existing credentials data from file Ok(PKIVaultPlugin { - plugin_dir: PathBuf::from(&config.plugin_dir), cert_details: config.pkivault_cert_details, - credblob_file: path, - cred_store: Arc::new(RwLock::new(credential)), + ca_store: Arc::new(RwLock::new(empty_cas)), }) } } @@ -345,14 +315,14 @@ impl TryFrom for PKIVaultPlugin { /// for a sandbox store and retrieve credentials specific to the sandbox. #[derive(Debug, PartialEq, serde::Deserialize)] pub struct SandboxParams { - /// Required: ID of a sandbox or pod - pub id: String, + /// Required: Token assigned to the Pod + pub token: String, - /// Required: IP of a sandbox or pod - pub ip: String, - - /// Required: Name of a sandbox or pod + /// Required: Pod name (unique within a namespace) pub name: String, + + /// Required: Pod IP address + pub ip: String, } impl TryFrom<&str> for SandboxParams { @@ -375,92 +345,92 @@ pub struct CredentialsOut { /// Manages the credentials generation, handling requests /// from backend, and credentials persistence storage pub struct PKIVaultPlugin { - pub plugin_dir: PathBuf, pub cert_details: PKIVaultCertDetails, - pub credblob_file: PathBuf, - pub cred_store: Arc>>, + pub ca_store: Arc>>, } impl PKIVaultPlugin { - fn get_credentials(&self, key: &str) -> Option { - let cred_store = self.cred_store.read().unwrap(); - cred_store.get(key).cloned() + fn construct_key(&self, params: &SandboxParams) -> String { + format!("{}_{}_{}", params.name, params.ip, params.token) } - fn store_credentials(&self, key: &str, credentials: Credentials) { - let mut cred_store = self.cred_store.write().unwrap(); - cred_store.insert(key.to_string(), credentials); + fn get_ca(&self, key: &str) -> Option { + let ca_store = self.ca_store.read().unwrap(); + ca_store.get(key).cloned() } - // Generate the credentials (keys and certs for ca, server, and client) - fn generate_credentials(&self, key: &str) -> Result> { - let credentials = Credentials::new(&self.cert_details)?; - - // Store the credentials into the hashmap - self.store_credentials(key, credentials.clone()); - - // Write the hashmap to file for a persistence copy - if let Err(e) = self.save_hashmap(&self.credblob_file) { - log::warn!("Failed to store credentials into file: {}", e); - } + fn store_ca(&self, key: &str, ca: PKIVaultCA) { + let mut ca_store = self.ca_store.write().unwrap(); + ca_store.insert(key.to_string(), ca); + } - log::info!("Returning newly generated credentials!"); - let resource = CredentialsOut { - key: credentials.server_key.clone(), - cert: credentials.server_cert.clone(), - ca_cert: credentials.ca_cert.clone(), - }; + async fn generate_pod_credentials(&self, params: &SandboxParams) -> Result> { + let key = self.construct_key(¶ms); - Ok(serde_json::to_vec(&resource)?) - } + // Return the stored CA credentials if they are present in the hashmap + let ca = if let Some(stored_ca) = self.get_ca(&key) { + log::info!("Generating credentials using existing CA!"); + PKIVaultCA::init(stored_ca.key, stored_ca.cert)? + } else { + log::info!("Generating credentials using new CA!"); + let new_ca = PKIVaultCA::new(&self.cert_details)?; - fn save_hashmap(&self, path: &PathBuf) -> Result<()> { - let cred_store = self.cred_store.read().unwrap(); - let serialized = serde_json::to_string(&*cred_store)?; - fs::write(path, serialized)?; - Ok(()) - } + // Store the newly created CA for future use + self.store_ca(&key, new_ca.clone()); - async fn get_server_credentials(&self, params: &SandboxParams) -> Result> { - // Return the server credentials if the credentials presents in the hashmap - let key = format!("{}_{}_{}", ¶ms.name, ¶ms.ip, ¶ms.id); - if let Some(credentials) = self.get_credentials(&key) { - log::info!("Returning existing credentials!"); + new_ca + }; - let resource = CredentialsOut { - key: credentials.server_key, - cert: credentials.server_cert, - ca_cert: credentials.ca_cert, - }; + let (pod_key, pod_cert) = ca.generate_credentials( + &X509::from_pem(&ca.cert)?, + &PKey::private_key_from_pem(&ca.key)?, + &self.cert_details, + "server", + )?; - return Ok(serde_json::to_vec(&resource)?); + let resource = CredentialsOut { + key: pod_key.private_key_to_pem_pkcs8()?, + cert: pod_cert.to_pem()?, + ca_cert: ca.cert, }; - // Otherwise return newly generated credentials - self.generate_credentials(&key) + Ok(serde_json::to_vec(&resource)?) } async fn list_pods(&self) -> Result> { - let cred_store = self.cred_store.read().unwrap(); - let keys: Vec = cred_store.keys().cloned().collect(); + let ca_store = self.ca_store.read().unwrap(); + let keys: Vec = ca_store.keys().cloned().collect(); Ok(serde_json::to_vec(&keys).expect("Failed to deserialize it!")) } - async fn get_client_credentials(&self, params: &SandboxParams) -> Result> { - let key = format!("{}_{}_{}", ¶ms.name, ¶ms.ip, ¶ms.id); - if let Some(credentials) = self.get_credentials(&key) { - log::info!("Found client credentials!"); + async fn generate_client_credentials(&self, params: &SandboxParams) -> Result> { + let key_hash = self.construct_key(¶ms); + + // Return the stored CA credentials if they are present in the hashmap + if let Some(stored_ca) = self.get_ca(&key_hash) { + log::info!("Returning client credentials!"); + + let ca = PKIVaultCA::init(stored_ca.key, stored_ca.cert)?; + + let (pod_key, pod_cert) = ca.generate_credentials( + &X509::from_pem(&ca.cert)?, + &PKey::private_key_from_pem(&ca.key)?, + &self.cert_details, + "server", + )?; let resource = CredentialsOut { - key: credentials.client_key, - cert: credentials.client_cert, - ca_cert: credentials.ca_cert, + key: pod_key.private_key_to_pem_pkcs8()?, + cert: pod_cert.to_pem()?, + ca_cert: ca.cert, }; return Ok(serde_json::to_vec(&resource)?); - }; + } else { + log::info!("Credentails cannot be generated. No CA found!"); - return Ok(serde_json::to_vec("")?); + return Ok(serde_json::to_vec("")?); + } } } @@ -478,28 +448,26 @@ impl ClientPlugin for PKIVaultPlugin { .context("accessed path is illegal, should start with `/`")?; match method.as_str() { - "GET" => { - match sub_path { - "credentials" => { - let params = SandboxParams::try_from(query)?; - let credentials = self.get_server_credentials(¶ms).await?; - - Ok(credentials) - } - "list_pods" => { - let pods = self.list_pods().await?; - - Ok(pods) - } - "get_client_credentials" => { - let params = SandboxParams::try_from(query)?; - let credentials = self.get_client_credentials(¶ms).await?; - - Ok(credentials) - } - _ => Err(anyhow!("{} not supported", sub_path))?, + "GET" => match sub_path { + "credentials" => { + let params = SandboxParams::try_from(query)?; + let credentials = self.generate_pod_credentials(¶ms).await?; + + Ok(credentials) } - } + "list_pods" => { + let pods = self.list_pods().await?; + + Ok(pods) + } + "client_credentials" => { + let params = SandboxParams::try_from(query)?; + let credentials = self.generate_client_credentials(¶ms).await?; + + Ok(credentials) + } + _ => Err(anyhow!("{} not supported", sub_path))?, + }, _ => bail!("Illegal HTTP method. Only supports `GET` and `POST`"), } } @@ -547,7 +515,6 @@ impl ClientPlugin for PKIVaultPlugin { } } - #[cfg(test)] mod tests { use super::*; @@ -560,7 +527,7 @@ mod tests { // Define sample inputs let body: &[u8] = b""; - let query = "id=3367348&ip=60.11.12.43&name=pod7"; + let query = "token=podToken12345&name=pod51&ip=60.11.12.89"; let path = "/credentials"; let method = &Method::GET; @@ -571,7 +538,7 @@ mod tests { match result { Ok(response) => { // Expected results - let key = String::from("pod7_60.11.12.43_3367348"); + let key = String::from("podToken12345_pod51_60.11.12.89"); if let Some(credentials) = plugin.get_credentials(&key) { let resource = CredentialsOut { @@ -587,4 +554,4 @@ mod tests { Err(e) => panic!("Expected Ok, got Err: {:?}", e), } } -} \ No newline at end of file +}