From d0b08fc03171048cebba7b853ba534b2a30bf567 Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Fri, 27 Sep 2024 14:41:51 +0800 Subject: [PATCH 01/10] KBS: refactor attestation module This refactoring combines all RCAR (attestation) related code into one module. This would help to better modularization and error handling. Signed-off-by: Xynnn007 --- kbs/src/attestation/backend.rs | 362 ++++++++++++++++++ kbs/src/attestation/coco/builtin.rs | 3 +- kbs/src/attestation/coco/grpc.rs | 9 +- kbs/src/attestation/config.rs | 29 ++ kbs/src/attestation/error.rs | 42 ++ .../attestation/intel_trust_authority/mod.rs | 7 +- kbs/src/attestation/mod.rs | 191 +-------- kbs/src/{ => attestation}/session.rs | 4 +- kbs/src/http/attest.rs | 153 -------- 9 files changed, 449 insertions(+), 351 deletions(-) create mode 100644 kbs/src/attestation/backend.rs create mode 100644 kbs/src/attestation/config.rs create mode 100644 kbs/src/attestation/error.rs rename kbs/src/{ => attestation}/session.rs (95%) delete mode 100644 kbs/src/http/attest.rs diff --git a/kbs/src/attestation/backend.rs b/kbs/src/attestation/backend.rs new file mode 100644 index 000000000..febc9e490 --- /dev/null +++ b/kbs/src/attestation/backend.rs @@ -0,0 +1,362 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; + +use actix_web::{HttpRequest, HttpResponse}; +use anyhow::{anyhow, bail, Context}; +use async_trait::async_trait; +use base64::{engine::general_purpose::STANDARD, Engine}; +use kbs_types::{Attestation, Challenge, Request, Tee}; +use lazy_static::lazy_static; +use log::{debug, info}; +use rand::{thread_rng, Rng}; +use semver::{BuildMetadata, Prerelease, Version, VersionReq}; +use serde::Deserialize; +use serde_json::json; + +use crate::attestation::session::KBS_SESSION_ID; + +use super::{ + config::{AttestationConfig, AttestationServiceConfig}, + session::{SessionMap, SessionStatus}, + Error, Result, +}; + +static KBS_MAJOR_VERSION: u64 = 0; +static KBS_MINOR_VERSION: u64 = 1; +static KBS_PATCH_VERSION: u64 = 1; + +lazy_static! { + static ref VERSION_REQ: VersionReq = { + let kbs_version = Version { + major: KBS_MAJOR_VERSION, + minor: KBS_MINOR_VERSION, + patch: KBS_PATCH_VERSION, + pre: Prerelease::EMPTY, + build: BuildMetadata::EMPTY, + }; + + VersionReq::parse(&format!("={kbs_version}")).unwrap() + }; +} + +/// Number of bytes in a nonce. +const NONCE_SIZE_BYTES: usize = 32; + +/// Create a nonce and return as a base-64 encoded string. +pub async fn make_nonce() -> anyhow::Result { + let mut nonce: Vec = vec![0; NONCE_SIZE_BYTES]; + + thread_rng() + .try_fill(&mut nonce[..]) + .map_err(anyhow::Error::from)?; + + Ok(STANDARD.encode(&nonce)) +} + +pub(crate) async fn generic_generate_challenge( + _tee: Tee, + _tee_parameters: serde_json::Value, +) -> anyhow::Result { + let nonce = make_nonce().await?; + + Ok(Challenge { + nonce, + extra_params: serde_json::Value::String(String::new()), + }) +} + +/// Interface for Attestation Services. +/// +/// Attestation Service implementations should implement this interface. +#[async_trait] +pub trait Attest: Send + Sync { + /// Set Attestation Policy + async fn set_policy(&self, _policy_id: &str, _policy: &str) -> anyhow::Result<()> { + Err(anyhow!("Set Policy API is unimplemented")) + } + + /// Verify Attestation Evidence + /// Return Attestation Results Token + async fn verify(&self, tee: Tee, nonce: &str, attestation: &str) -> anyhow::Result; + + /// generate the Challenge to pass to attester based on Tee and nonce + async fn generate_challenge( + &self, + tee: Tee, + tee_parameters: serde_json::Value, + ) -> anyhow::Result { + generic_generate_challenge(tee, tee_parameters).await + } +} + +/// Attestation Service +#[derive(Clone)] +pub struct AttestationService { + /// Attestation Module + inner: Arc, + + /// A concurrent safe map to keep status of RCAR status + session_map: Arc, + + /// Maximum session expiration time. + timeout: i64, +} + +#[derive(Deserialize, Debug)] +pub struct SetPolicyInput { + policy_id: String, + policy: String, +} + +impl AttestationService { + pub async fn new(config: AttestationConfig) -> Result { + let inner = match config.attestation_service { + #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] + AttestationServiceConfig::CoCoASBuiltIn(cfg) => { + let built_in_as = super::coco::builtin::BuiltInCoCoAs::new(cfg) + .await + .map_err(|e| Error::AttestationServiceInitialization { source: e })?; + Arc::new(built_in_as) as _ + } + #[cfg(feature = "coco-as-grpc")] + AttestationServiceConfig::CoCoASGrpc(cfg) => { + let grpc_coco_as = super::coco::grpc::GrpcClientPool::new(cfg) + .await + .map_err(|e| Error::AttestationServiceInitialization { source: e })?; + Arc::new(grpc_coco_as) as _ + } + #[cfg(feature = "intel-trust-authority-as")] + AttestationServiceConfig::IntelTA(cfg) => { + let intel_ta = super::intel_trust_authority::IntelTrustAuthority::new(cfg) + .await + .map_err(|e| Error::AttestationServiceInitialization { source: e })?; + Arc::new(intel_ta) as _ + } + }; + + let session_map = Arc::new(SessionMap::new()); + + tokio::spawn({ + let session_map_clone = session_map.clone(); + async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(60)).await; + session_map_clone + .sessions + .retain_async(|_, v| !v.is_expired()) + .await; + } + } + }); + Ok(Self { + inner, + timeout: config.timeout, + session_map, + }) + } + + pub async fn set_policy(&self, request: &[u8]) -> Result<()> { + self.__set_policy(request) + .await + .map_err(|e| Error::SetPolicy { source: e }) + } + + async fn __set_policy(&self, request: &[u8]) -> anyhow::Result<()> { + let input: SetPolicyInput = + serde_json::from_slice(request).context("parse set policy request")?; + self.inner.set_policy(&input.policy_id, &input.policy).await + } + + pub async fn auth(&self, request: &[u8]) -> Result { + self.__auth(request) + .await + .map_err(|e| Error::RcarAuthFailed { source: e }) + } + + async fn __auth(&self, request: &[u8]) -> anyhow::Result { + let request: Request = serde_json::from_slice(request).context("deserialize Request")?; + let version = Version::parse(&request.version).context("failed to parse KBS version")?; + if !VERSION_REQ.matches(&version) { + bail!( + "expected version: {}, requested version: {}", + *VERSION_REQ, + request.version + ); + } + + let challenge = self + .inner + .generate_challenge(request.tee, request.extra_params.clone()) + .await + .context("generate challenge")?; + + let session = SessionStatus::auth(request, self.timeout, challenge).context("Session")?; + + let response = HttpResponse::Ok() + .cookie(session.cookie()) + .json(session.challenge()); + + self.session_map.insert(session); + + Ok(response) + } + + pub async fn attest(&self, attestation: &[u8], request: HttpRequest) -> Result { + self.__attest(attestation, request) + .await + .map_err(|e| Error::RcarAttestFailed { source: e }) + } + + async fn __attest( + &self, + attestation: &[u8], + request: HttpRequest, + ) -> anyhow::Result { + let cookie = request.cookie(KBS_SESSION_ID).context("cookie not found")?; + + let session_id = cookie.value(); + + let attestation: Attestation = + serde_json::from_slice(attestation).context("deserialize Attestation")?; + let (tee, nonce) = { + let session = self + .session_map + .sessions + .get_async(session_id) + .await + .ok_or(anyhow!("No cookie found"))?; + let session = session.get(); + + debug!("Session ID {}", session.id()); + + if session.is_expired() { + bail!("session expired."); + } + + if let SessionStatus::Attested { token, .. } = session { + debug!( + "Session {} is already attested. Skip attestation and return the old token", + session.id() + ); + let body = serde_json::to_string(&json!({ + "token": token, + })) + .context("Serialize token failed")?; + + return Ok(HttpResponse::Ok() + .cookie(session.cookie()) + .content_type("application/json") + .body(body)); + } + + let attestation_str = serde_json::to_string_pretty(&attestation) + .context("Failed to serialize Attestation")?; + debug!("Attestation: {attestation_str}"); + + (session.request().tee, session.challenge().nonce.to_string()) + }; + + let attestation_str = + serde_json::to_string(&attestation).context("serialize attestation failed")?; + let token = self + .inner + .verify(tee, &nonce, &attestation_str) + .await + .context("verify TEE evidence failed")?; + + let mut session = self + .session_map + .sessions + .get_async(session_id) + .await + .ok_or(anyhow!("session not found"))?; + let session = session.get_mut(); + + let body = serde_json::to_string(&json!({ + "token": token, + })) + .context("Serialize token failed")?; + + session.attest(token); + + Ok(HttpResponse::Ok() + .cookie(session.cookie()) + .content_type("application/json") + .body(body)) + } + + pub async fn get_attest_token_from_session( + &self, + request: &HttpRequest, + ) -> anyhow::Result { + let cookie = request + .cookie(KBS_SESSION_ID) + .context("KBS session cookie not found")?; + + let session = self + .session_map + .sessions + .get_async(cookie.value()) + .await + .context("session not found")?; + + let session = session.get(); + + info!("Cookie {} request to get resource", session.id()); + + if session.is_expired() { + bail!("The session is expired"); + } + + let SessionStatus::Attested { token, .. } = session else { + bail!("The session is not authorized"); + }; + + Ok(token.to_owned()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_make_nonce() { + const BITS_PER_BYTE: usize = 8; + + /// A base-64 encoded value is this many bits in length. + const BASE64_BITS_CHUNK: usize = 6; + + /// Number of bytes that base64 encoding requires the result to align on. + const BASE64_ROUNDING_MULTIPLE: usize = 4; + + /// The nominal base64 encoded length. + const BASE64_NONCE_LENGTH_UNROUNDED_BYTES: usize = + (NONCE_SIZE_BYTES * BITS_PER_BYTE) / BASE64_BITS_CHUNK; + + /// The actual base64 encoded length is rounded up to the specified multiple. + const EXPECTED_LENGTH_BYTES: usize = + BASE64_NONCE_LENGTH_UNROUNDED_BYTES.next_multiple_of(BASE64_ROUNDING_MULTIPLE); + + // Number of nonce tests to run (arbitrary) + let nonce_count = 13; + + let mut nonces = vec![]; + + for _ in 0..nonce_count { + let nonce = make_nonce().await.unwrap(); + + assert_eq!(nonce.len(), EXPECTED_LENGTH_BYTES); + + let found = nonces.contains(&nonce); + + // The nonces should be unique + assert_eq!(found, false); + + nonces.push(nonce); + } + } +} diff --git a/kbs/src/attestation/coco/builtin.rs b/kbs/src/attestation/coco/builtin.rs index cc0bdcf9d..a6b9faaf0 100644 --- a/kbs/src/attestation/coco/builtin.rs +++ b/kbs/src/attestation/coco/builtin.rs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use crate::attestation::{make_nonce, Attest}; use anyhow::*; use async_trait::async_trait; use attestation_service::{config::Config as AsConfig, AttestationService, Data, HashAlgorithm}; @@ -10,6 +9,8 @@ use kbs_types::{Attestation, Challenge, Tee}; use serde_json::json; use tokio::sync::RwLock; +use crate::attestation::backend::{make_nonce, Attest}; + pub struct BuiltInCoCoAs { inner: RwLock, } diff --git a/kbs/src/attestation/coco/grpc.rs b/kbs/src/attestation/coco/grpc.rs index 903dbf344..7e4d9a3da 100644 --- a/kbs/src/attestation/coco/grpc.rs +++ b/kbs/src/attestation/coco/grpc.rs @@ -2,23 +2,20 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use crate::attestation::{make_nonce, Attest}; use anyhow::*; use async_trait::async_trait; -use base64::{ - engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, - Engine, -}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use kbs_types::{Attestation, Challenge, Tee}; use log::info; use mobc::{Manager, Pool}; -use rand::{thread_rng, Rng}; use serde::Deserialize; use serde_json::json; use std::collections::HashMap; use tokio::sync::Mutex; use tonic::transport::Channel; +use crate::attestation::backend::{make_nonce, Attest}; + use self::attestation::{ attestation_request::RuntimeData, attestation_service_client::AttestationServiceClient, AttestationRequest, ChallengeRequest, SetPolicyRequest, diff --git a/kbs/src/attestation/config.rs b/kbs/src/attestation/config.rs new file mode 100644 index 000000000..f49be5a3b --- /dev/null +++ b/kbs/src/attestation/config.rs @@ -0,0 +1,29 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use serde::Deserialize; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct AttestationConfig { + #[serde(flatten)] + pub attestation_service: AttestationServiceConfig, + + pub timeout: i64, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum AttestationServiceConfig { + #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] + #[serde(alias = "coco_as_builtin")] + CoCoASBuiltIn(attestation_service::config::Config), + + #[cfg(feature = "coco-as-grpc")] + #[serde(alias = "coco_as_grpc")] + CoCoASGrpc(super::coco::grpc::GrpcConfig), + + #[cfg(feature = "intel-trust-authority-as")] + #[serde(alias = "intel_ta")] + IntelTA(super::intel_trust_authority::IntelTrustAuthorityConfig), +} diff --git a/kbs/src/attestation/error.rs b/kbs/src/attestation/error.rs new file mode 100644 index 000000000..c00bf1396 --- /dev/null +++ b/kbs/src/attestation/error.rs @@ -0,0 +1,42 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use log::error; +use strum::AsRefStr; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, AsRefStr, Debug)] +pub enum Error { + #[error("Failed to initialize attestation service")] + AttestationServiceInitialization { + #[source] + source: anyhow::Error, + }, + + #[error("Failed to extract Tee public key from claims")] + ExtractTeePubKeyFailed { + #[source] + source: anyhow::Error, + }, + + #[error("RCAR handshake Auth failed")] + RcarAuthFailed { + #[source] + source: anyhow::Error, + }, + + #[error("RCAR handshake Attest failed")] + RcarAttestFailed { + #[source] + source: anyhow::Error, + }, + + #[error("Set Attestation Policy failed")] + SetPolicy { + #[source] + source: anyhow::Error, + }, +} diff --git a/kbs/src/attestation/intel_trust_authority/mod.rs b/kbs/src/attestation/intel_trust_authority/mod.rs index 7986fd4ec..bbfe6f889 100644 --- a/kbs/src/attestation/intel_trust_authority/mod.rs +++ b/kbs/src/attestation/intel_trust_authority/mod.rs @@ -2,9 +2,10 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use super::Attest; -use crate::attestation::{generic_generate_challenge, make_nonce}; -use crate::token::{jwk::JwkAttestationTokenVerifier, AttestationTokenVerifierConfig}; +use crate::{ + attestation::backend::{generic_generate_challenge, make_nonce, Attest}, + token::{jwk::JwkAttestationTokenVerifier, AttestationTokenVerifierConfig}, +}; use anyhow::*; use async_trait::async_trait; use az_cvm_vtpm::hcl::HclReport; diff --git a/kbs/src/attestation/mod.rs b/kbs/src/attestation/mod.rs index 6141304f8..10d9ca88d 100644 --- a/kbs/src/attestation/mod.rs +++ b/kbs/src/attestation/mod.rs @@ -2,196 +2,17 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use anyhow::*; -use async_trait::async_trait; -#[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] -use attestation_service::config::Config as AsConfig; -use base64::{engine::general_purpose::STANDARD, Engine}; -#[cfg(feature = "coco-as-grpc")] -use coco::grpc::*; -#[cfg(feature = "intel-trust-authority-as")] -use intel_trust_authority::*; -use kbs_types::{Challenge, Tee}; -use rand::{thread_rng, Rng}; - -#[cfg(not(feature = "intel-trust-authority-as"))] -pub const AS_TOKEN_TEE_PUBKEY_PATH: &str = "/customized_claims/runtime_data/tee-pubkey"; -#[cfg(feature = "intel-trust-authority-as")] -pub const AS_TOKEN_TEE_PUBKEY_PATH: &str = "/attester_runtime_data/tee-pubkey"; - #[cfg(feature = "coco-as")] -#[allow(missing_docs)] pub mod coco; #[cfg(feature = "intel-trust-authority-as")] pub mod intel_trust_authority; -/// Number of bytes in a nonce. -const NONCE_SIZE_BYTES: usize = 32; - -/// Create a nonce and return as a base-64 encoded string. -pub async fn make_nonce() -> Result { - let mut nonce: Vec = vec![0; NONCE_SIZE_BYTES]; - - thread_rng() - .try_fill(&mut nonce[..]) - .map_err(anyhow::Error::from)?; - - Ok(STANDARD.encode(&nonce)) -} - -pub(crate) async fn generic_generate_challenge( - _tee: Tee, - _tee_parameters: serde_json::Value, -) -> Result { - let nonce = make_nonce().await?; - - Ok(Challenge { - nonce, - extra_params: serde_json::Value::String(String::new()), - }) -} - -/// Interface for Attestation Services. -/// -/// Attestation Service implementations should implement this interface. -#[async_trait] -pub trait Attest: Send + Sync { - /// Set Attestation Policy - async fn set_policy(&self, _policy_id: &str, _policy: &str) -> Result<()> { - Err(anyhow!("Set Policy API is unimplemented")) - } - - /// Verify Attestation Evidence - /// Return Attestation Results Token - async fn verify(&self, tee: Tee, nonce: &str, attestation: &str) -> Result; - - /// generate the Challenge to pass to attester based on Tee and nonce - async fn generate_challenge( - &self, - tee: Tee, - tee_parameters: serde_json::Value, - ) -> Result { - generic_generate_challenge(tee, tee_parameters).await - } -} - -/// Attestation Service -pub enum AttestationService { - #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] - CoCoASBuiltIn(coco::builtin::BuiltInCoCoAs), - - #[cfg(feature = "coco-as-grpc")] - CoCoASgRPC(GrpcClientPool), - - #[cfg(feature = "intel-trust-authority-as")] - IntelTA(IntelTrustAuthority), -} - -impl AttestationService { - /// Create and initialize AttestationService. - #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] - pub async fn new(config: AsConfig) -> Result { - let built_in_as = coco::builtin::BuiltInCoCoAs::new(config).await?; - Ok(Self::CoCoASBuiltIn(built_in_as)) - } - - /// Create and initialize AttestationService. - #[cfg(feature = "coco-as-grpc")] - pub async fn new(config: GrpcConfig) -> Result { - let pool = GrpcClientPool::new(config).await?; - Ok(Self::CoCoASgRPC(pool)) - } - - /// Create and initialize AttestationService. - #[cfg(feature = "intel-trust-authority-as")] - pub async fn new(config: IntelTrustAuthorityConfig) -> Result { - let ta_client = intel_trust_authority::IntelTrustAuthority::new(config).await?; - Ok(Self::IntelTA(ta_client)) - } - - pub async fn verify(&self, tee: Tee, nonce: &str, attestation: &str) -> Result { - match self { - #[cfg(feature = "coco-as-grpc")] - AttestationService::CoCoASgRPC(inner) => inner.verify(tee, nonce, attestation).await, - #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] - AttestationService::CoCoASBuiltIn(inner) => inner.verify(tee, nonce, attestation).await, - #[cfg(feature = "intel-trust-authority-as")] - AttestationService::IntelTA(inner) => inner.verify(tee, nonce, attestation).await, - } - } - - pub async fn set_policy(&self, policy_id: &str, policy: &str) -> Result<()> { - match self { - #[cfg(feature = "coco-as-grpc")] - AttestationService::CoCoASgRPC(inner) => inner.set_policy(policy_id, policy).await, - #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] - AttestationService::CoCoASBuiltIn(inner) => inner.set_policy(policy_id, policy).await, - #[cfg(feature = "intel-trust-authority-as")] - AttestationService::IntelTA(inner) => inner.set_policy(policy_id, policy).await, - } - } - - pub async fn generate_challenge( - &self, - tee: Tee, - tee_parameters: serde_json::Value, - ) -> Result { - match self { - #[cfg(feature = "coco-as-grpc")] - AttestationService::CoCoASgRPC(inner) => { - inner.generate_challenge(tee, tee_parameters).await - } - #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] - AttestationService::CoCoASBuiltIn(inner) => { - inner.generate_challenge(tee, tee_parameters).await - } - #[cfg(feature = "intel-trust-authority-as")] - AttestationService::IntelTA(inner) => { - inner.generate_challenge(tee, tee_parameters).await - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_make_nonce() { - const BITS_PER_BYTE: usize = 8; - - /// A base-64 encoded value is this many bits in length. - const BASE64_BITS_CHUNK: usize = 6; - - /// Number of bytes that base64 encoding requires the result to align on. - const BASE64_ROUNDING_MULTIPLE: usize = 4; - - /// The nominal base64 encoded length. - const BASE64_NONCE_LENGTH_UNROUNDED_BYTES: usize = - (NONCE_SIZE_BYTES * BITS_PER_BYTE) / BASE64_BITS_CHUNK; - - /// The actual base64 encoded length is rounded up to the specified multiple. - const EXPECTED_LENGTH_BYTES: usize = - BASE64_NONCE_LENGTH_UNROUNDED_BYTES.next_multiple_of(BASE64_ROUNDING_MULTIPLE); - - // Number of nonce tests to run (arbitrary) - let nonce_count = 13; - - let mut nonces = vec![]; - - for _ in 0..nonce_count { - let nonce = make_nonce().await.unwrap(); - - assert_eq!(nonce.len(), EXPECTED_LENGTH_BYTES); - - let found = nonces.contains(&nonce); +pub mod backend; +pub mod config; +pub mod session; - // The nonces should be unique - assert_eq!(found, false); +pub use backend::AttestationService; - nonces.push(nonce); - } - } -} +pub mod error; +pub use error::*; diff --git a/kbs/src/session.rs b/kbs/src/attestation/session.rs similarity index 95% rename from kbs/src/session.rs rename to kbs/src/attestation/session.rs index ec2169d3b..50892776c 100644 --- a/kbs/src/session.rs +++ b/kbs/src/attestation/session.rs @@ -23,7 +23,6 @@ pub(crate) enum SessionStatus { }, Attested { - attestation_claims: String, token: String, id: String, timeout: OffsetDateTime, @@ -85,11 +84,10 @@ impl SessionStatus { return *self.timeout() < OffsetDateTime::now_utc(); } - pub fn attest(&mut self, attestation_claims: String, token: String) { + pub fn attest(&mut self, token: String) { match self { SessionStatus::Authed { id, timeout, .. } => { *self = SessionStatus::Attested { - attestation_claims, token, id: id.clone(), timeout: *timeout, diff --git a/kbs/src/http/attest.rs b/kbs/src/http/attest.rs deleted file mode 100644 index f4de7e87d..000000000 --- a/kbs/src/http/attest.rs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2022 by Rivos Inc. -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use crate::{raise_error, session::SessionStatus}; - -use super::*; - -use anyhow::anyhow; -use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; -use base64::Engine; -use kbs_types::Challenge; -use log::{debug, error, info}; -use semver::{BuildMetadata, Prerelease, Version, VersionReq}; -use serde_json::json; - -static KBS_MAJOR_VERSION: u64 = 0; -static KBS_MINOR_VERSION: u64 = 1; -static KBS_PATCH_VERSION: u64 = 1; - -lazy_static! { - static ref VERSION_REQ: VersionReq = { - let kbs_version = Version { - major: KBS_MAJOR_VERSION, - minor: KBS_MINOR_VERSION, - patch: KBS_PATCH_VERSION, - pre: Prerelease::EMPTY, - build: BuildMetadata::EMPTY, - }; - - VersionReq::parse(&format!("={kbs_version}")).unwrap() - }; -} - -/// POST /auth -pub(crate) async fn auth( - request: web::Json, - map: web::Data, - timeout: web::Data, - attestation_service: web::Data>, -) -> Result { - info!("Auth API called."); - debug!("Auth Request: {:?}", &request); - let version = Version::parse(&request.version).unwrap(); - if !VERSION_REQ.matches(&version) { - raise_error!(Error::ProtocolVersion(format!( - "expected version: {}, requested version: {}", - *VERSION_REQ, - request.version.clone() - ))); - } - - let challenge = attestation_service - .generate_challenge(request.tee, request.extra_params.clone()) - .await - .map_err(|e| Error::FailedAuthentication(format!("generate challenge: {e:?}")))?; - - let session = SessionStatus::auth(request.0, **timeout, challenge) - .map_err(|e| Error::FailedAuthentication(format!("Session: {e}")))?; - - let response = HttpResponse::Ok() - .cookie(session.cookie()) - .json(session.challenge()); - - map.insert(session); - - Ok(response) -} - -/// POST /attest -pub(crate) async fn attest( - attestation: web::Json, - request: HttpRequest, - map: web::Data, - attestation_service: web::Data>, -) -> Result { - info!("Attest API called."); - let cookie = request.cookie(KBS_SESSION_ID).ok_or(Error::MissingCookie)?; - - let (tee, nonce) = { - let session = map - .sessions - .get_async(cookie.value()) - .await - .ok_or(Error::InvalidCookie)?; - let session = session.get(); - - debug!("Session ID {}", session.id()); - - if session.is_expired() { - raise_error!(Error::ExpiredCookie); - } - - if let SessionStatus::Attested { token, .. } = session { - debug!( - "Session {} is already attested. Skip attestation and return the old token", - session.id() - ); - let body = serde_json::to_string(&json!({ - "token": token, - })) - .map_err(|e| Error::TokenIssueFailed(format!("Serialize token failed {e}")))?; - - return Ok(HttpResponse::Ok() - .cookie(session.cookie()) - .content_type("application/json") - .body(body)); - } - - let attestation_str = serde_json::to_string_pretty(&attestation.0) - .map_err(|_| Error::AttestationFailed("Failed to serialize Attestation".into()))?; - debug!("Attestation: {attestation_str}"); - - (session.request().tee, session.challenge().nonce.to_string()) - }; - - let attestation_str = serde_json::to_string(&attestation) - .map_err(|e| Error::AttestationFailed(format!("serialize attestation failed : {e:?}")))?; - let token = attestation_service - .verify(tee, &nonce, &attestation_str) - .await - .map_err(|e| Error::AttestationFailed(format!("{e:?}")))?; - - let claims_b64 = token - .split('.') - .nth(1) - .ok_or_else(|| Error::TokenIssueFailed("Illegal token format".to_string()))?; - let claims = String::from_utf8( - URL_SAFE_NO_PAD - .decode(claims_b64) - .map_err(|e| Error::TokenIssueFailed(format!("Illegal token base64 claims: {e}")))?, - ) - .map_err(|e| Error::TokenIssueFailed(format!("Illegal token base64 claims: {e}")))?; - - let mut session = map - .sessions - .get_async(cookie.value()) - .await - .ok_or(Error::InvalidCookie)?; - let session = session.get_mut(); - - let body = serde_json::to_string(&json!({ - "token": token, - })) - .map_err(|e| Error::TokenIssueFailed(format!("Serialize token failed {e}")))?; - - session.attest(claims, token); - - Ok(HttpResponse::Ok() - .cookie(session.cookie()) - .content_type("application/json") - .body(body)) -} From 51b37bbe2a01d564745a7d2a92e75971d962f973 Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Fri, 27 Sep 2024 14:54:23 +0800 Subject: [PATCH 02/10] KBS: refactor policy engine module This commit does some refactoring upon policy engine module. Including 1. Change ResourcePolicyError to PolicyEngineError. This is because in future, different client plugins would share same policy engine thus the new name will match better. 2. add a new `set_policy` api for PolicyEngine. This api will handle SetPolicyInput format request rather than the plaintext of policy. This would help to integrate into the KBS server. The plugin mechanism is by default enabled, thus we delete `opa` and `policy` feature. By default integrate `regorus` crate for policy. Signed-off-by: Xynnn007 --- kbs/Cargo.toml | 6 -- kbs/config/kubernetes/base/policy.rego | 2 +- kbs/config/kubernetes/ita/policy.rego | 2 +- kbs/src/policy_engine/error.rs | 36 +++++++ kbs/src/policy_engine/mod.rs | 95 +++++++++---------- kbs/src/policy_engine/opa/default_policy.rego | 2 +- kbs/src/policy_engine/opa/mod.rs | 60 ++++++------ 7 files changed, 119 insertions(+), 84 deletions(-) create mode 100644 kbs/src/policy_engine/error.rs diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index 6ff78dd42..69e5ac914 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -18,12 +18,6 @@ as = [] # Use CoCo-AS as backend attestation service coco-as = ["as"] -# Support resource policy for KBS -policy = [] - -# Use OPA/Rego as resource policy for KBS -opa = ["policy"] - # Use built-in CoCo-AS as backend attestation service coco-as-builtin = ["coco-as", "attestation-service/default"] diff --git a/kbs/config/kubernetes/base/policy.rego b/kbs/config/kubernetes/base/policy.rego index d9f9eb532..c369cf7ce 100644 --- a/kbs/config/kubernetes/base/policy.rego +++ b/kbs/config/kubernetes/base/policy.rego @@ -15,7 +15,7 @@ # # The variable is a KBS resource path, # which is required to be a string in three segment path format://, -# for example: "my'repo/License/key". +# for example: "repo/License/key". # # The format of Attestation Claims Input is defined by the attestation service, # and its format may look like the following: diff --git a/kbs/config/kubernetes/ita/policy.rego b/kbs/config/kubernetes/ita/policy.rego index 3c179197d..e940d3f22 100644 --- a/kbs/config/kubernetes/ita/policy.rego +++ b/kbs/config/kubernetes/ita/policy.rego @@ -15,7 +15,7 @@ # # The variable is a KBS resource path, # which is required to be a string in three segment path format://, -# for example: "my'repo/License/key". +# for example: "repo/License/key". # # The format of Attestation Claims Input is defined by the attestation service, # and its format may look like the following: diff --git a/kbs/src/policy_engine/error.rs b/kbs/src/policy_engine/error.rs new file mode 100644 index 000000000..0970b4b5e --- /dev/null +++ b/kbs/src/policy_engine/error.rs @@ -0,0 +1,36 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use log::error; +use strum::AsRefStr; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, AsRefStr, Debug)] +pub enum KbsPolicyEngineError { + #[error("Failed to evaluate policy {0}")] + EvaluationError(#[from] anyhow::Error), + + #[error("Failed to load data for policy")] + DataLoadError, + + #[error("Invalid resource path format")] + ResourcePathError, + + #[error("Policy IO Error: {0}")] + IOError(#[from] std::io::Error), + + #[error("Decoding (base64) policy failed: {0}")] + DecodeError(#[from] base64::DecodeError), + + #[error("Failed to load input for policy")] + InputError, + + #[error("Failed to load policy")] + PolicyLoadError, + + #[error("Set Policy request is illegal for {0}")] + IllegalSetPolicyRequest(&'static str), +} diff --git a/kbs/src/policy_engine/mod.rs b/kbs/src/policy_engine/mod.rs index 29d6926e3..995dea2bd 100644 --- a/kbs/src/policy_engine/mod.rs +++ b/kbs/src/policy_engine/mod.rs @@ -2,63 +2,41 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use anyhow::Result; use async_trait::async_trait; use serde::Deserialize; +use serde_json::Value; +use tokio::sync::Mutex; + use std::path::PathBuf; use std::sync::Arc; -use thiserror::Error; -use tokio::sync::Mutex; -#[cfg(feature = "opa")] mod opa; -const DEFAULT_POLICY_PATH: &str = "/opa/confidential-containers/kbs/policy.rego"; - -#[derive(Error, Debug)] -pub enum ResourcePolicyError { - #[error("Failed to evaluate resource policy {0}")] - EvaluationError(#[from] anyhow::Error), - - #[error("Failed to load data for resource policy")] - DataLoadError, +mod error; +pub use error::*; - #[error("Invalid resource path format")] - ResourcePathError, - - #[error("Resource Policy IO Error: {0}")] - IOError(#[from] std::io::Error), - - #[error("Decoding (base64) resource policy failed: {0}")] - DecodeError(#[from] base64::DecodeError), - - #[error("Failed to load input for resource policy")] - InputError, - - #[error("Failed to load resource policy")] - PolicyLoadError, -} +pub const DEFAULT_POLICY_PATH: &str = "/opt/confidential-containers/kbs/policy.rego"; /// Resource policy engine interface +/// +/// TODO: Use a better authentication and authorization policy #[async_trait] pub(crate) trait PolicyEngineInterface: Send + Sync { - /// Determine whether there is access to a specific path resource based on the input claims. + /// Determine whether there is access to a specific path based on the input claims. /// Input parameters: - /// resource_path: Required to be a string in three segment path format://, for example: "my'repo/License/key". + /// request_path: Required to be a string in segments path format:/.../, for example: "my'repo/License/key". /// input_claims: Parsed claims from Attestation Token. /// /// return value: - /// ([decide_result, extra_output]) + /// (decide_result) /// decide_result: Boolean value to present whether the evaluate is passed or not. - /// extra_output: original ouput from policy engine. - async fn evaluate( - &self, - resource_path: String, - input_claims: String, - ) -> Result; + async fn evaluate(&self, request_path: &str, input_claims: &str) -> Result; /// Set policy (Base64 encode) - async fn set_policy(&mut self, policy: String) -> Result<(), ResourcePolicyError>; + async fn set_policy(&mut self, policy: &str) -> Result<()>; + + /// Get policy (Base64 encode) + async fn get_policy(&self) -> Result; } /// Policy engine configuration. @@ -83,16 +61,37 @@ pub(crate) struct PolicyEngine(pub Arc>); impl PolicyEngine { /// Create and initialize PolicyEngine - pub async fn new(config: &PolicyEngineConfig) -> Result { - let policy_engine: Arc> = { - cfg_if::cfg_if! { - if #[cfg(feature = "opa")] { - Arc::new(Mutex::new(opa::Opa::new(config.policy_path.clone().unwrap_or(PathBuf::from(DEFAULT_POLICY_PATH)))?)) - } else { - compile_error!("Please enable at least one of the following features: `opa` to continue."); - } - } - }; + pub async fn new(config: &PolicyEngineConfig) -> Result { + let policy_engine: Arc> = + Arc::new(Mutex::new(opa::Opa::new(config.policy_path.clone())?)); Ok(Self(policy_engine)) } + + pub async fn evaluate(&self, request_path: &str, input_claims: &str) -> Result { + self.0 + .lock() + .await + .evaluate(request_path, input_claims) + .await + } + + pub async fn set_policy(&self, request: &[u8]) -> Result<()> { + let request: Value = serde_json::from_slice(request).map_err(|_| { + KbsPolicyEngineError::IllegalSetPolicyRequest("Illegal SetPolicy Request Json") + })?; + let policy = request + .pointer("/policy") + .ok_or(KbsPolicyEngineError::IllegalSetPolicyRequest( + "No `policy` field inside SetPolicy Request Json", + ))? + .as_str() + .ok_or(KbsPolicyEngineError::IllegalSetPolicyRequest( + "`policy` field is not a string in SetPolicy Request Json", + ))?; + self.0.lock().await.set_policy(policy).await + } + + pub async fn get_policy(&self) -> Result { + self.0.lock().await.get_policy().await + } } diff --git a/kbs/src/policy_engine/opa/default_policy.rego b/kbs/src/policy_engine/opa/default_policy.rego index 207717bda..f3470f42e 100644 --- a/kbs/src/policy_engine/opa/default_policy.rego +++ b/kbs/src/policy_engine/opa/default_policy.rego @@ -15,7 +15,7 @@ # # The variable is a KBS resource path, # which is required to be a string in three segment path format://, -# for example: "my'repo/License/key". +# for example: "repo/License/key". # # The format of Attestation Claims Input is defined by the attestation service, # and its format may look like the following: diff --git a/kbs/src/policy_engine/opa/mod.rs b/kbs/src/policy_engine/opa/mod.rs index 054d920cc..372ec54e0 100644 --- a/kbs/src/policy_engine/opa/mod.rs +++ b/kbs/src/policy_engine/opa/mod.rs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use crate::policy_engine::{PolicyEngineInterface, ResourcePolicyError}; +use crate::policy_engine::{KbsPolicyEngineError, PolicyEngineInterface}; use async_trait::async_trait; use base64::Engine; use std::fs; @@ -14,7 +14,7 @@ pub struct Opa { } impl Opa { - pub fn new(policy_path: PathBuf) -> Result { + pub fn new(policy_path: PathBuf) -> Result { std::fs::create_dir_all(policy_path.parent().unwrap())?; if !policy_path.as_path().exists() { @@ -30,41 +30,47 @@ impl Opa { impl PolicyEngineInterface for Opa { async fn evaluate( &self, - resource_path: String, - input_claims: String, - ) -> Result { + resource_path: &str, + input_claims: &str, + ) -> Result { let mut engine = regorus::Engine::new(); // Add policy as data engine .add_policy_from_file(self.policy_path.clone()) - .map_err(|_| ResourcePolicyError::PolicyLoadError)?; + .map_err(|_| KbsPolicyEngineError::PolicyLoadError)?; // Add resource path as data let resource_path_object = regorus::Value::from_json_str(&format!("{{\"resource-path\":\"{}\"}}", resource_path)) - .map_err(|_| ResourcePolicyError::ResourcePathError)?; + .map_err(|_| KbsPolicyEngineError::ResourcePathError)?; engine .add_data(resource_path_object) - .map_err(|_| ResourcePolicyError::DataLoadError)?; + .map_err(|_| KbsPolicyEngineError::DataLoadError)?; // Add TCB claims as input engine - .set_input_json(&input_claims) - .map_err(|_| ResourcePolicyError::InputError)?; + .set_input_json(input_claims) + .map_err(|_| KbsPolicyEngineError::InputError)?; let res = engine.eval_bool_query("data.policy.allow".to_string(), false)?; Ok(res) } - async fn set_policy(&mut self, policy: String) -> Result<(), ResourcePolicyError> { + async fn set_policy(&mut self, policy: &str) -> Result<(), KbsPolicyEngineError> { let policy_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(policy)?; tokio::fs::write(&self.policy_path, policy_bytes).await?; Ok(()) } + + async fn get_policy(&self) -> Result { + let policy = tokio::fs::read(&self.policy_path).await?; + let policy = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(policy); + Ok(policy) + } } #[cfg(test)] @@ -76,20 +82,20 @@ mod tests { use serde_json::json; use tempfile::{NamedTempFile, TempDir}; - fn compare_errors(a: ResourcePolicyError, b: ResourcePolicyError) -> bool { + fn compare_errors(a: KbsPolicyEngineError, b: KbsPolicyEngineError) -> bool { match (a, b) { ( - ResourcePolicyError::EvaluationError(_a), - ResourcePolicyError::EvaluationError(_b), + KbsPolicyEngineError::EvaluationError(_a), + KbsPolicyEngineError::EvaluationError(_b), ) => true, - (ResourcePolicyError::DataLoadError, ResourcePolicyError::DataLoadError) => true, - (ResourcePolicyError::ResourcePathError, ResourcePolicyError::ResourcePathError) => { + (KbsPolicyEngineError::DataLoadError, KbsPolicyEngineError::DataLoadError) => true, + (KbsPolicyEngineError::ResourcePathError, KbsPolicyEngineError::ResourcePathError) => { true } - (ResourcePolicyError::IOError(_a), ResourcePolicyError::IOError(_b)) => true, - (ResourcePolicyError::DecodeError(_a), ResourcePolicyError::DecodeError(_b)) => true, - (ResourcePolicyError::InputError, ResourcePolicyError::InputError) => true, - (ResourcePolicyError::PolicyLoadError, ResourcePolicyError::PolicyLoadError) => true, + (KbsPolicyEngineError::IOError(_a), KbsPolicyEngineError::IOError(_b)) => true, + (KbsPolicyEngineError::DecodeError(_a), KbsPolicyEngineError::DecodeError(_b)) => true, + (KbsPolicyEngineError::InputError, KbsPolicyEngineError::InputError) => true, + (KbsPolicyEngineError::PolicyLoadError, KbsPolicyEngineError::PolicyLoadError) => true, _ => false, } } @@ -106,7 +112,7 @@ mod tests { .to_string() } - async fn set_policy_from_file(opa: &mut Opa, path: &str) -> Result<(), ResourcePolicyError> { + async fn set_policy_from_file(opa: &mut Opa, path: &str) -> Result<(), KbsPolicyEngineError> { let policy = std::fs::read(PathBuf::from(path.to_string())).unwrap(); let policy = URL_SAFE_NO_PAD.encode(policy); @@ -128,7 +134,7 @@ mod tests { let res = opa.set_policy(malformed_policy).await; assert!(matches!( res.err().unwrap(), - ResourcePolicyError::DecodeError(base64::DecodeError::InvalidLastSymbol(_, _)) + KbsPolicyEngineError::DecodeError(base64::DecodeError::InvalidLastSymbol(_, _)) )); // IOError @@ -136,7 +142,7 @@ mod tests { let res = set_policy_from_file(&mut opa, "test/data/policy_1.rego").await; assert!(matches!( res.err().unwrap(), - ResourcePolicyError::IOError(_) + KbsPolicyEngineError::IOError(_) )); } @@ -150,21 +156,21 @@ mod tests { "\"", "", 1, - Err(ResourcePolicyError::ResourcePathError) + Err(KbsPolicyEngineError::ResourcePathError) )] #[case( "test/data/policy_invalid_1.rego", "my_repo/Alice/key", "Alice", 1, - Err(ResourcePolicyError::PolicyLoadError) + Err(KbsPolicyEngineError::PolicyLoadError) )] #[case( "test/data/policy_invalid_2.rego", "my_repo/Alice/key", "Alice", 1, - Err(ResourcePolicyError::EvaluationError(anyhow::anyhow!("test"))) + Err(KbsPolicyEngineError::EvaluationError(anyhow::anyhow!("test"))) )] #[case("test/data/policy_5.rego", "myrepo/secret/secret1", "n", 2, Ok(true))] #[case("test/data/policy_5.rego", "myrepo/secret/secret1", "n", 1, Ok(false))] @@ -179,7 +185,7 @@ mod tests { #[case] resource_path: &str, #[case] input_name: &str, #[case] input_svn: u64, - #[case] expected: Result, + #[case] expected: Result, ) { let tmp_file = NamedTempFile::new().unwrap(); let mut opa = Opa::new(tmp_file.path().to_path_buf()).unwrap(); From a7735449fb5cfc91b99f468cb01c1b4209639d76 Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Fri, 27 Sep 2024 14:58:15 +0800 Subject: [PATCH 03/10] KBS: add Admin auth module This module brings all admin authentication logic together. Currently it allows to use a public key to verify the admin access. Signed-off-by: Xynnn007 --- kbs/src/admin/config.rs | 30 +++++++++++++++++++++ kbs/src/admin/error.rs | 30 +++++++++++++++++++++ kbs/src/admin/mod.rs | 58 +++++++++++++++++++++++++++++++++++++++++ kbs/src/auth.rs | 25 ------------------ 4 files changed, 118 insertions(+), 25 deletions(-) create mode 100644 kbs/src/admin/config.rs create mode 100644 kbs/src/admin/error.rs create mode 100644 kbs/src/admin/mod.rs delete mode 100644 kbs/src/auth.rs diff --git a/kbs/src/admin/config.rs b/kbs/src/admin/config.rs new file mode 100644 index 000000000..050ef250c --- /dev/null +++ b/kbs/src/admin/config.rs @@ -0,0 +1,30 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use std::path::PathBuf; + +use serde::Deserialize; + +pub const DEFAULT_INSECURE_API: bool = false; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct AdminConfig { + /// Public key used to authenticate the resource registration endpoint token (JWT). + /// Only JWTs signed with the corresponding private keys are authenticated. + pub auth_public_key: Option, + + /// Insecure HTTP APIs. + /// WARNING: Using this option enables KBS insecure APIs such as Resource Registration without + /// verifying the JWK. + pub insecure_api: bool, +} + +impl Default for AdminConfig { + fn default() -> Self { + Self { + auth_public_key: None, + insecure_api: DEFAULT_INSECURE_API, + } + } +} diff --git a/kbs/src/admin/error.rs b/kbs/src/admin/error.rs new file mode 100644 index 000000000..2c21f631d --- /dev/null +++ b/kbs/src/admin/error.rs @@ -0,0 +1,30 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use log::error; +use strum::AsRefStr; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, AsRefStr, Debug)] +pub enum Error { + #[error("Admin Token verification failed")] + JwtVerificationFailed { + #[source] + source: jwt_simple::Error, + }, + + #[error("`auth_public_key` is not set in the config file")] + NoPublicKeyGiven, + + #[error("Failed to parse admin public key")] + ParsePublicKey(#[from] jwt_simple::Error), + + #[error("Failed to parse HTTP Auth Bearer header")] + ParseAuthHeaderFailed(#[from] actix_web::error::ParseError), + + #[error("Read admin public key failed")] + ReadPublicKey(#[from] std::io::Error), +} diff --git a/kbs/src/admin/mod.rs b/kbs/src/admin/mod.rs new file mode 100644 index 000000000..f5a376a7b --- /dev/null +++ b/kbs/src/admin/mod.rs @@ -0,0 +1,58 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use actix_web::{http::header::Header, HttpRequest}; +use actix_web_httpauth::headers::authorization::{Authorization, Bearer}; +use config::AdminConfig; +use jwt_simple::{ + claims::NoCustomClaims, + common::VerificationOptions, + prelude::{Ed25519PublicKey, EdDSAPublicKeyLike}, +}; + +pub mod config; +pub mod error; +pub use error::*; +use log::warn; + +#[derive(Default, Clone)] +pub struct Admin { + public_key: Option, +} + +impl TryFrom for Admin { + type Error = Error; + + fn try_from(value: AdminConfig) -> Result { + if value.insecure_api { + warn!("insecure admin APIs are enabled"); + return Ok(Admin::default()); + } + + let key_path = value.auth_public_key.ok_or(Error::NoPublicKeyGiven)?; + let user_public_key_pem = std::fs::read_to_string(key_path)?; + let key = Ed25519PublicKey::from_pem(&user_public_key_pem)?; + Ok(Self { + public_key: Some(key), + }) + } +} + +impl Admin { + pub(crate) fn validate_auth(&self, request: &HttpRequest) -> Result<()> { + let Some(public_key) = &self.public_key else { + return Ok(()); + }; + + let bearer = Authorization::::parse(request)?.into_scheme(); + + let token = bearer.token(); + + let _claims = public_key + .verify_token::(token, Some(VerificationOptions::default())) + .map_err(|e| Error::JwtVerificationFailed { source: e })?; + + Ok(()) + } +} diff --git a/kbs/src/auth.rs b/kbs/src/auth.rs deleted file mode 100644 index 053f22ce1..000000000 --- a/kbs/src/auth.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2023 by Alibaba. -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use actix_web::http::header::Header; -use actix_web::HttpRequest; -use actix_web_httpauth::headers::authorization::{Authorization, Bearer}; -use anyhow::{Context, Result}; -use jwt_simple::prelude::{ - Ed25519PublicKey, EdDSAPublicKeyLike, NoCustomClaims, VerificationOptions, -}; - -pub(crate) fn validate_auth(request: &HttpRequest, public_key: &Ed25519PublicKey) -> Result<()> { - let bearer = Authorization::::parse(request) - .context("parse Authorization header failed")? - .into_scheme(); - - let token = bearer.token(); - - let _claims = public_key - .verify_token::(token, Some(VerificationOptions::default())) - .context("token verification failed")?; - - Ok(()) -} From 08ef166b0d9e652539b2accae0dfa9b2a4335b6d Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Fri, 27 Sep 2024 15:01:26 +0800 Subject: [PATCH 04/10] KBS: add Plugins module The Plugins module could provide a plugin way for developers to extend the ability of KBS client APIs. This also provides a Sample implementation for example. The oroginal resource is refactored into plugins as a plugin. Also, we change both `read_secret_resource` and `write_secret_resource` to Fn rather than FnMut. This leaves the synchronization handling to concrete underlying plugins, thus promote the performance because we can avoid a global Mutex. Signed-off-by: Xynnn007 --- Cargo.lock | 13 +- kbs/Cargo.toml | 1 + kbs/src/http/resource.rs | 240 ------------------ kbs/src/jwe.rs | 65 +++++ kbs/src/plugins/implementations/mod.rs | 9 + .../implementations}/resource/aliyun_kms.rs | 14 +- .../implementations/resource/backend.rs | 157 ++++++++++++ .../implementations}/resource/local_fs.rs | 50 ++-- .../plugins/implementations/resource/mod.rs | 73 ++++++ kbs/src/plugins/implementations/sample.rs | 64 +++++ kbs/src/plugins/mod.rs | 10 + kbs/src/plugins/plugin_manager.rs | 120 +++++++++ kbs/src/resource/mod.rs | 106 -------- 13 files changed, 544 insertions(+), 378 deletions(-) delete mode 100644 kbs/src/http/resource.rs create mode 100644 kbs/src/jwe.rs create mode 100644 kbs/src/plugins/implementations/mod.rs rename kbs/src/{ => plugins/implementations}/resource/aliyun_kms.rs (79%) create mode 100644 kbs/src/plugins/implementations/resource/backend.rs rename kbs/src/{ => plugins/implementations}/resource/local_fs.rs (64%) create mode 100644 kbs/src/plugins/implementations/resource/mod.rs create mode 100644 kbs/src/plugins/implementations/sample.rs create mode 100644 kbs/src/plugins/mod.rs create mode 100644 kbs/src/plugins/plugin_manager.rs delete mode 100644 kbs/src/resource/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 03f9f5554..242b387e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2676,6 +2676,7 @@ dependencies = [ "openssl", "prost 0.12.6", "rand", + "regex", "regorus", "reqwest 0.12.8", "rsa 0.9.6", @@ -4075,9 +4076,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -4087,9 +4088,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -4098,9 +4099,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "regorus" diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index 69e5ac914..6414ebdc1 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -54,6 +54,7 @@ log.workspace = true mobc = { version = "0.8.3", optional = true } prost = { workspace = true, optional = true } rand = "0.8.5" +regex = "1.11.1" regorus.workspace = true reqwest = { workspace = true, features = ["json"], optional = true } rsa = { version = "0.9.2", optional = true, features = ["sha2"] } diff --git a/kbs/src/http/resource.rs b/kbs/src/http/resource.rs deleted file mode 100644 index ee5e6e66b..000000000 --- a/kbs/src/http/resource.rs +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) 2022 by Rivos Inc. -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use actix_web::{http::header::Header, web::Bytes}; -use actix_web_httpauth::headers::authorization::{Authorization, Bearer}; -use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce}; -use anyhow::{anyhow, bail}; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use base64::Engine; -use kbs_types::{Response, TeePubKey}; -use log::{debug, error, info}; -use rand::{rngs::OsRng, Rng}; -use rsa::{BigUint, Pkcs1v15Encrypt, RsaPublicKey}; -use serde::Deserialize; -use serde_json::{json, Deserializer, Value}; - -use crate::{raise_error, token::TokenVerifier}; - -use super::*; - -#[allow(unused_assignments)] -/// GET /resource/{repository}/{type}/{tag} -/// GET /resource/{type}/{tag} -pub(crate) async fn get_resource( - request: HttpRequest, - repository: web::Data>>, - #[cfg(feature = "as")] map: web::Data, - token_verifier: web::Data, - #[cfg(feature = "policy")] policy_engine: web::Data, -) -> Result { - #[allow(unused_mut)] - let mut claims_option = None; - #[cfg(feature = "as")] - { - claims_option = get_attest_claims_from_session(&request, map).await.ok(); - } - let claims_str = if let Some(c) = claims_option { - debug!("Get pkey from session."); - c - } else { - debug!("Get pkey from auth header"); - get_attest_claims_from_header(&request, &token_verifier).await? - }; - let claims: Value = serde_json::from_str(&claims_str).map_err(|e| { - Error::AttestationClaimsParseFailed(format!("illegal attestation claims: {e}")) - })?; - - let pubkey = token_verifier.extract_tee_public_key(claims).map_err(|e| { - Error::AttestationClaimsParseFailed(format!( - "Failed to extract public key in attestation claims: {e:?}" - )) - })?; - - let resource_description = ResourceDesc { - repository_name: request - .match_info() - .get("repository") - .unwrap_or("default") - .to_string(), - resource_type: request - .match_info() - .get("type") - .ok_or_else(|| Error::InvalidRequest(String::from("no `type` in url")))? - .to_string(), - resource_tag: request - .match_info() - .get("tag") - .ok_or_else(|| Error::InvalidRequest(String::from("no `tag` in url")))? - .to_string(), - }; - - if !resource_description.is_valid() { - return Err(Error::InvalidRequest("Invalid resource path".to_string())); - } - - info!( - "Get resource from kbs:///{}/{}/{}", - resource_description.repository_name, - resource_description.resource_type, - resource_description.resource_tag - ); - - #[cfg(feature = "policy")] - { - let resource_path = format!( - "{}/{}/{}", - resource_description.repository_name, - resource_description.resource_type, - resource_description.resource_tag - ); - let resource_allowed = policy_engine - .0 - .lock() - .await - .evaluate(resource_path, claims_str) - .await - .map_err(|e| Error::PolicyEngineFailed(e.to_string()))?; - - if !resource_allowed { - raise_error!(Error::PolicyReject); - } - - info!("Resource access request passes policy check."); - } - - let resource_byte = repository - .read() - .await - .read_secret_resource(resource_description) - .await - .map_err(|e| Error::ReadSecretFailed(format!("{e:?}")))?; - - let jwe = jwe(pubkey, resource_byte)?; - - let res = serde_json::to_string(&jwe).map_err(|e| Error::JWEFailed(e.to_string()))?; - - Ok(HttpResponse::Ok() - .content_type("application/json") - .body(res)) -} - -#[cfg(feature = "as")] -async fn get_attest_claims_from_session( - request: &HttpRequest, - map: web::Data, -) -> Result { - // check cookie - - use crate::session::SessionStatus; - let cookie = request - .cookie(KBS_SESSION_ID) - .ok_or(Error::UnAuthenticatedCookie)?; - - let session = map - .sessions - .get_async(cookie.value()) - .await - .ok_or(Error::UnAuthenticatedCookie)?; - - let session = session.get(); - - info!("Cookie {} request to get resource", session.id()); - - if session.is_expired() { - error!("Expired KBS cookie {}", cookie.value()); - raise_error!(Error::ExpiredCookie); - } - - let SessionStatus::Attested { - attestation_claims, .. - } = session - else { - raise_error!(Error::UnAuthenticatedCookie); - }; - - Ok(attestation_claims.to_owned()) -} - -async fn get_attest_claims_from_header( - request: &HttpRequest, - token_verifier: &web::Data, -) -> Result { - let bearer = Authorization::::parse(request) - .map_err(|e| Error::InvalidRequest(format!("parse Authorization header failed: {e}")))? - .into_scheme(); - - let token = bearer.token().to_string(); - - let claims = token_verifier - .verify(token) - .await - .map_err(|e| Error::TokenParseFailed(format!("verify token failed: {e:?}")))?; - let claims = serde_json::to_string(&claims) - .map_err(|_| Error::TokenParseFailed("failed to serialize claims".into()))?; - Ok(claims) -} - -const RSA_ALGORITHM: &str = "RSA1_5"; -const AES_GCM_256_ALGORITHM: &str = "A256GCM"; - -pub(crate) fn jwe(tee_pub_key: TeePubKey, payload_data: Vec) -> Result { - let TeePubKey::RSA { alg, k_mod, k_exp } = tee_pub_key else { - raise_error!(Error::JWEFailed(format!( - "key type is not TeePubKey::RSA but {:?}", - tee_pub_key - ))); - }; - - if alg != *RSA_ALGORITHM { - raise_error!(Error::JWEFailed(format!( - "algorithm is not {RSA_ALGORITHM} but {}", - alg - ))); - } - - let mut rng = rand::thread_rng(); - - let aes_sym_key = Aes256Gcm::generate_key(&mut OsRng); - let cipher = Aes256Gcm::new(&aes_sym_key); - let iv = rng.gen::<[u8; 12]>(); - let nonce = Nonce::from_slice(&iv); - let encrypted_payload_data = cipher - .encrypt(nonce, payload_data.as_slice()) - .map_err(|e| Error::JWEFailed(format!("AES encrypt Resource payload failed: {e:?}")))?; - - let k_mod = URL_SAFE_NO_PAD - .decode(k_mod) - .map_err(|e| Error::JWEFailed(format!("base64 decode k_mod failed: {e:?}")))?; - let n = BigUint::from_bytes_be(&k_mod); - let k_exp = URL_SAFE_NO_PAD - .decode(k_exp) - .map_err(|e| Error::JWEFailed(format!("base64 decode k_exp failed: {e:?}")))?; - let e = BigUint::from_bytes_be(&k_exp); - - let rsa_pub_key = RsaPublicKey::new(n, e).map_err(|e| { - Error::JWEFailed(format!( - "Building RSA key from modulus and exponent failed: {e:?}" - )) - })?; - let sym_key: &[u8] = aes_sym_key.as_slice(); - let wrapped_sym_key = rsa_pub_key - .encrypt(&mut rng, Pkcs1v15Encrypt, sym_key) - .map_err(|e| Error::JWEFailed(format!("RSA encrypt sym key failed: {e:?}")))?; - - let protected_header = json!( - { - "alg": RSA_ALGORITHM.to_string(), - "enc": AES_GCM_256_ALGORITHM.to_string(), - }); - - Ok(Response { - protected: serde_json::to_string(&protected_header) - .map_err(|e| Error::JWEFailed(format!("serde protected_header failed: {e}")))?, - encrypted_key: URL_SAFE_NO_PAD.encode(wrapped_sym_key), - iv: URL_SAFE_NO_PAD.encode(iv), - ciphertext: URL_SAFE_NO_PAD.encode(encrypted_payload_data), - tag: "".to_string(), - }) -} diff --git a/kbs/src/jwe.rs b/kbs/src/jwe.rs new file mode 100644 index 000000000..44e68d1b1 --- /dev/null +++ b/kbs/src/jwe.rs @@ -0,0 +1,65 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce}; +use anyhow::{anyhow, bail, Context, Result}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use kbs_types::{Response, TeePubKey}; +use rand::{rngs::OsRng, Rng}; +use rsa::{BigUint, Pkcs1v15Encrypt, RsaPublicKey}; +use serde_json::json; + +const RSA_ALGORITHM: &str = "RSA1_5"; +const AES_GCM_256_ALGORITHM: &str = "A256GCM"; + +pub fn jwe(tee_pub_key: TeePubKey, payload_data: Vec) -> Result { + let TeePubKey::RSA { alg, k_mod, k_exp } = tee_pub_key else { + bail!("Only RSA key is support for TEE pub key") + }; + + if alg != *RSA_ALGORITHM { + bail!("algorithm is not {RSA_ALGORITHM} but {alg}"); + } + + let mut rng = rand::thread_rng(); + + let aes_sym_key = Aes256Gcm::generate_key(&mut OsRng); + let cipher = Aes256Gcm::new(&aes_sym_key); + let iv = rng.gen::<[u8; 12]>(); + let nonce = Nonce::from_slice(&iv); + let encrypted_payload_data = cipher + .encrypt(nonce, payload_data.as_slice()) + .map_err(|e| anyhow!("AES encrypt Resource payload failed: {e}"))?; + + let k_mod = URL_SAFE_NO_PAD + .decode(k_mod) + .context("base64 decode k_mod failed")?; + let n = BigUint::from_bytes_be(&k_mod); + let k_exp = URL_SAFE_NO_PAD + .decode(k_exp) + .context("base64 decode k_exp failed")?; + let e = BigUint::from_bytes_be(&k_exp); + + let rsa_pub_key = + RsaPublicKey::new(n, e).context("Building RSA key from modulus and exponent failed")?; + let sym_key: &[u8] = aes_sym_key.as_slice(); + let wrapped_sym_key = rsa_pub_key + .encrypt(&mut rng, Pkcs1v15Encrypt, sym_key) + .context("RSA encrypt sym key failed")?; + + let protected_header = json!( + { + "alg": RSA_ALGORITHM.to_string(), + "enc": AES_GCM_256_ALGORITHM.to_string(), + }); + + Ok(Response { + protected: serde_json::to_string(&protected_header) + .context("serde protected_header failed")?, + encrypted_key: URL_SAFE_NO_PAD.encode(wrapped_sym_key), + iv: URL_SAFE_NO_PAD.encode(iv), + ciphertext: URL_SAFE_NO_PAD.encode(encrypted_payload_data), + tag: "".to_string(), + }) +} diff --git a/kbs/src/plugins/implementations/mod.rs b/kbs/src/plugins/implementations/mod.rs new file mode 100644 index 000000000..8bf856bbf --- /dev/null +++ b/kbs/src/plugins/implementations/mod.rs @@ -0,0 +1,9 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +pub mod resource; +pub mod sample; + +pub use resource::{RepositoryConfig, ResourceStorage}; +pub use sample::{Sample, SampleConfig}; diff --git a/kbs/src/resource/aliyun_kms.rs b/kbs/src/plugins/implementations/resource/aliyun_kms.rs similarity index 79% rename from kbs/src/resource/aliyun_kms.rs rename to kbs/src/plugins/implementations/resource/aliyun_kms.rs index 0c380f67f..50833412d 100644 --- a/kbs/src/resource/aliyun_kms.rs +++ b/kbs/src/plugins/implementations/resource/aliyun_kms.rs @@ -2,13 +2,13 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use super::{Repository, ResourceDesc}; -use anyhow::{Context, Result}; +use super::{Error, Repository, ResourceDesc, Result}; +use anyhow::Context; use kms::{plugins::aliyun::AliyunKmsClient, Annotations, Getter}; use log::info; use serde::Deserialize; -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, PartialEq)] pub struct AliyunKmsBackendConfig { client_key: String, kms_instance_id: String, @@ -32,12 +32,13 @@ impl Repository for AliyunKmsBackend { .client .get_secret(&name, &Annotations::default()) .await - .context("failed to get resource from aliyun KMS")?; + .context("failed to get resource from aliyun KMS") + .map_err(|e| Error::AliyunError { source: e })?; Ok(resource_bytes) } async fn write_secret_resource( - &mut self, + &self, _resource_desc: ResourceDesc, _data: &[u8], ) -> Result<()> { @@ -53,7 +54,8 @@ impl AliyunKmsBackend { &repo_desc.password, &repo_desc.cert_pem, ) - .context("create aliyun KMS backend")?; + .context("create aliyun KMS backend") + .map_err(|e| Error::AliyunError { source: e })?; Ok(Self { client }) } } diff --git a/kbs/src/plugins/implementations/resource/backend.rs b/kbs/src/plugins/implementations/resource/backend.rs new file mode 100644 index 000000000..c1228c8ff --- /dev/null +++ b/kbs/src/plugins/implementations/resource/backend.rs @@ -0,0 +1,157 @@ +// Copyright (c) 2023 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::{Arc, OnceLock}; + +use anyhow::{bail, Context, Error, Result}; +use regex::Regex; +use serde::Deserialize; + +use super::local_fs; + +type RepositoryInstance = Arc; + +/// Interface of a `Repository`. +#[async_trait::async_trait] +pub trait StorageBackend: Send + Sync { + /// Read secret resource from repository. + async fn read_secret_resource(&self, resource_desc: ResourceDesc) -> Result>; + + /// Write secret resource into repository + async fn write_secret_resource(&self, resource_desc: ResourceDesc, data: &[u8]) -> Result<()>; +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceDesc { + pub repository_name: String, + pub resource_type: String, + pub resource_tag: String, +} + +static CELL: OnceLock = OnceLock::new(); + +impl TryFrom<&str> for ResourceDesc { + type Error = Error; + + fn try_from(value: &str) -> Result { + let regex = CELL.get_or_init(|| { + Regex::new( + r"^((?[a-zA-Z0-9_\-]+)/)?(?[a-zA-Z0-9_\-]+)/(?[a-zA-Z0-9_\-]+)$", + ) + .unwrap() + }); + let Some(captures) = regex.captures(value) else { + bail!("illegal ResourceDesc format."); + }; + + Ok(Self { + repository_name: captures + .name("repo") + .map(|s| s.into()) + .unwrap_or("default") + .into(), + resource_type: captures["type"].into(), + resource_tag: captures["tag"].into(), + }) + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum RepositoryConfig { + LocalFs(local_fs::LocalFsRepoDesc), + + #[cfg(feature = "aliyun")] + #[serde(alias = "aliyun")] + Aliyun(super::aliyun_kms::AliyunKmsBackendConfig), +} + +impl Default for RepositoryConfig { + fn default() -> Self { + Self::LocalFs(local_fs::LocalFsRepoDesc::default()) + } +} + +#[derive(Clone)] +pub struct ResourceStorage { + backend: RepositoryInstance, +} + +impl TryFrom for ResourceStorage { + type Error = Error; + + fn try_from(value: RepositoryConfig) -> Result { + match value { + RepositoryConfig::LocalFs(desc) => { + let backend = local_fs::LocalFs::new(&desc) + .context("Failed to initialize Resource Storage")?; + Ok(Self { + backend: Arc::new(backend), + }) + } + #[cfg(feature = "aliyun")] + RepositoryConfig::Aliyun(config) => { + let client = super::aliyun_kms::AliyunKmsBackend::new(&config)?; + Ok(Self { + backend: Arc::new(client), + }) + } + } + } +} + +impl ResourceStorage { + pub(crate) async fn set_secret_resource( + &self, + resource_desc: ResourceDesc, + data: &[u8], + ) -> Result<()> { + self.backend + .write_secret_resource(resource_desc, data) + .await + } + + pub(crate) async fn get_secret_resource(&self, resource_desc: ResourceDesc) -> Result> { + self.backend.read_secret_resource(resource_desc).await + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::ResourceDesc; + + #[rstest] + #[case("default/1/2", Some(ResourceDesc { + repository_name: "default".into(), + resource_type: "1".into(), + resource_tag: "2".into(), + }))] + #[case("/1/2", None)] + #[case("/repo/type/tag", None)] + #[case("repo/type/tag", Some(ResourceDesc { + repository_name: "repo".into(), + resource_type: "type".into(), + resource_tag: "tag".into(), + }))] + #[case("1/2", Some(ResourceDesc { + repository_name: "default".into(), + resource_type: "1".into(), + resource_tag: "2".into(), + }))] + #[case("123--_default/1Abff-_/___-afds44BC", Some(ResourceDesc { + repository_name: "123--_default".into(), + resource_type: "1Abff-_".into(), + resource_tag: "___-afds44BC".into(), + }))] + fn parse_resource_desc(#[case] desc: &str, #[case] expected: Option) { + let parsed = ResourceDesc::try_from(desc); + if expected.is_none() { + assert!(parsed.is_err()); + } else { + assert_eq!(parsed.unwrap(), expected.unwrap()); + } + } +} diff --git a/kbs/src/resource/local_fs.rs b/kbs/src/plugins/implementations/resource/local_fs.rs similarity index 64% rename from kbs/src/resource/local_fs.rs rename to kbs/src/plugins/implementations/resource/local_fs.rs index 99640be00..8ec7201a1 100644 --- a/kbs/src/resource/local_fs.rs +++ b/kbs/src/plugins/implementations/resource/local_fs.rs @@ -2,22 +2,25 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use super::{Repository, ResourceDesc}; +use super::{ResourceDesc, StorageBackend}; use anyhow::{Context, Result}; use serde::Deserialize; -use std::path::{Path, PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, +}; pub const DEFAULT_REPO_DIR_PATH: &str = "/opt/confidential-containers/kbs/repository"; -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, PartialEq)] pub struct LocalFsRepoDesc { - pub dir_path: Option, + pub dir_path: String, } impl Default for LocalFsRepoDesc { fn default() -> Self { Self { - dir_path: Some(DEFAULT_REPO_DIR_PATH.to_string()), + dir_path: DEFAULT_REPO_DIR_PATH.into(), } } } @@ -27,7 +30,7 @@ pub struct LocalFs { } #[async_trait::async_trait] -impl Repository for LocalFs { +impl StorageBackend for LocalFs { async fn read_secret_resource(&self, resource_desc: ResourceDesc) -> Result> { let mut resource_path = PathBuf::from(&self.repo_dir_path); @@ -43,11 +46,7 @@ impl Repository for LocalFs { Ok(resource_byte) } - async fn write_secret_resource( - &mut self, - resource_desc: ResourceDesc, - data: &[u8], - ) -> Result<()> { + async fn write_secret_resource(&self, resource_desc: ResourceDesc, data: &[u8]) -> Result<()> { let mut resource_path = PathBuf::from(&self.repo_dir_path); resource_path.push(resource_desc.repository_name); resource_path.push(resource_desc.resource_type); @@ -60,6 +59,11 @@ impl Repository for LocalFs { resource_path.push(resource_desc.resource_tag); + // Note that the local fs does not handle synchronization conditions + // because it is only for test use case and we assume the write request + // will not happen togetherly with reads. + // If it is to be used in productive scenarios, it is recommended that + // the storage is marked as read-only and written out-of-band. tokio::fs::write(resource_path, data) .await .context("write local fs") @@ -67,21 +71,27 @@ impl Repository for LocalFs { } impl LocalFs { - pub fn new(repo_desc: &LocalFsRepoDesc) -> Result { + pub fn new(repo_desc: &LocalFsRepoDesc) -> anyhow::Result { + // Create repository dir. + if !Path::new(&repo_desc.dir_path).exists() { + fs::create_dir_all(&repo_desc.dir_path)?; + } + // Create default repo. + if !Path::new(&format!("{}/default", &repo_desc.dir_path)).exists() { + fs::create_dir_all(format!("{}/default", &repo_desc.dir_path))?; + } + Ok(Self { - repo_dir_path: repo_desc - .dir_path - .clone() - .unwrap_or(DEFAULT_REPO_DIR_PATH.to_string()), + repo_dir_path: repo_desc.dir_path.clone(), }) } } #[cfg(test)] mod tests { - use crate::resource::{ + use super::super::{ local_fs::{LocalFs, LocalFsRepoDesc}, - Repository, ResourceDesc, + ResourceDesc, StorageBackend, }; const TEST_DATA: &[u8] = b"testdata"; @@ -90,10 +100,10 @@ mod tests { async fn write_and_read_resource() { let tmp_dir = tempfile::tempdir().expect("create temp dir failed"); let repo_desc = LocalFsRepoDesc { - dir_path: Some(tmp_dir.path().to_string_lossy().to_string()), + dir_path: tmp_dir.path().to_string_lossy().to_string(), }; - let mut local_fs = LocalFs::new(&repo_desc).expect("create local fs failed"); + let local_fs = LocalFs::new(&repo_desc).expect("create local fs failed"); let resource_desc = ResourceDesc { repository_name: "default".into(), resource_type: "test".into(), diff --git a/kbs/src/plugins/implementations/resource/mod.rs b/kbs/src/plugins/implementations/resource/mod.rs new file mode 100644 index 000000000..a3b90b1fd --- /dev/null +++ b/kbs/src/plugins/implementations/resource/mod.rs @@ -0,0 +1,73 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +pub mod local_fs; + +#[cfg(feature = "aliyun")] +pub mod aliyun_kms; + +use actix_web::http::Method; +use anyhow::{bail, Context, Result}; + +pub mod backend; +pub use backend::*; + +use super::super::plugin_manager::ClientPlugin; + +#[async_trait::async_trait] +impl ClientPlugin for ResourceStorage { + async fn handle( + &self, + body: &[u8], + _query: &str, + path: &str, + method: &Method, + ) -> Result> { + let resource_desc = path + .strip_prefix('/') + .context("accessed path is illegal, should start with `/`")?; + match method.as_str() { + "POST" => { + let resource_description = ResourceDesc::try_from(resource_desc)?; + self.set_secret_resource(resource_description, body).await?; + Ok(vec![]) + } + "GET" => { + let resource_description = ResourceDesc::try_from(resource_desc)?; + let resource = self.get_secret_resource(resource_description).await?; + + Ok(resource) + } + _ => bail!("Illegal HTTP method. Only supports `GET` and `POST`"), + } + } + + async fn validate_auth( + &self, + _body: &[u8], + _query: &str, + _path: &str, + method: &Method, + ) -> Result { + if method.as_str() == "POST" { + return Ok(true); + } + + Ok(false) + } + + async fn encrypted( + &self, + _body: &[u8], + _query: &str, + _path: &str, + method: &Method, + ) -> Result { + if method.as_str() == "GET" { + return Ok(true); + } + + Ok(false) + } +} diff --git a/kbs/src/plugins/implementations/sample.rs b/kbs/src/plugins/implementations/sample.rs new file mode 100644 index 000000000..ad04b7249 --- /dev/null +++ b/kbs/src/plugins/implementations/sample.rs @@ -0,0 +1,64 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +//! This is a sample to implement a client plugin + +use actix_web::http::Method; +use anyhow::Result; +use serde::Deserialize; + +use super::super::plugin_manager::ClientPlugin; + +#[derive(Deserialize, Clone, Debug, PartialEq)] +pub struct SampleConfig { + pub item: String, +} + +pub struct Sample { + _item: String, +} + +impl TryFrom for Sample { + type Error = anyhow::Error; + + fn try_from(value: SampleConfig) -> anyhow::Result { + Ok(Self { _item: value.item }) + } +} + +#[async_trait::async_trait] +impl ClientPlugin for Sample { + async fn handle( + &self, + _body: &[u8], + _query: &str, + _path: &str, + _method: &Method, + ) -> Result> { + Ok("sample plugin response".as_bytes().to_vec()) + } + + async fn validate_auth( + &self, + _body: &[u8], + _query: &str, + _path: &str, + _method: &Method, + ) -> Result { + Ok(true) + } + + /// 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) + } +} diff --git a/kbs/src/plugins/mod.rs b/kbs/src/plugins/mod.rs new file mode 100644 index 000000000..ec0bdf59e --- /dev/null +++ b/kbs/src/plugins/mod.rs @@ -0,0 +1,10 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +pub mod plugin_manager; + +pub mod implementations; +pub use implementations::*; + +pub use plugin_manager::{PluginManager, PluginsConfig}; diff --git a/kbs/src/plugins/plugin_manager.rs b/kbs/src/plugins/plugin_manager.rs new file mode 100644 index 000000000..f558aa64f --- /dev/null +++ b/kbs/src/plugins/plugin_manager.rs @@ -0,0 +1,120 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use std::{collections::HashMap, fmt::Display, sync::Arc}; + +use actix_web::http::Method; +use anyhow::{Context, Error, Result}; +use serde::Deserialize; + +use super::{sample, RepositoryConfig, ResourceStorage}; + +type ClientPluginInstance = Arc; + +#[async_trait::async_trait] +pub trait ClientPlugin: Send + Sync { + /// This function is the entry to a client plugin. The function + /// marks `&self` rather than `&mut self`, because it will leave + /// state and synchronization issues down to the concrete plugin. + /// + /// TODO: change body from Vec slice into Reader to apply for large + /// body stream. + async fn handle( + &self, + body: &[u8], + query: &str, + path: &str, + method: &Method, + ) -> Result>; + + /// Whether the concrete request needs to validate the admin auth. + /// If returns `Ok(true)`, the KBS server will perform an admin auth + /// validation before handle the request. + async fn validate_auth( + &self, + body: &[u8], + query: &str, + path: &str, + method: &Method, + ) -> Result; + + /// 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; +} + +#[derive(Deserialize, Clone, Debug, PartialEq)] +#[serde(tag = "name")] +pub enum PluginsConfig { + #[serde(alias = "sample")] + Sample(sample::SampleConfig), + + #[serde(alias = "resource")] + ResourceStorage(RepositoryConfig), +} + +impl Display for PluginsConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PluginsConfig::Sample(_) => f.write_str("sample"), + PluginsConfig::ResourceStorage(_) => f.write_str("resource"), + } + } +} + +impl TryInto for PluginsConfig { + type Error = Error; + + fn try_into(self) -> Result { + let plugin = match self { + PluginsConfig::Sample(cfg) => { + let sample_plugin = + sample::Sample::try_from(cfg).context("Initialize 'Sample' plugin failed")?; + Arc::new(sample_plugin) as _ + } + PluginsConfig::ResourceStorage(repository_config) => { + let resource_storage = ResourceStorage::try_from(repository_config) + .context("Initialize 'Resource' plugin failed")?; + Arc::new(resource_storage) as _ + } + }; + + Ok(plugin) + } +} + +/// [`PluginManager`] manages different kinds of plugins. +#[derive(Clone)] +pub struct PluginManager { + plugins: HashMap, +} + +impl TryFrom> for PluginManager { + type Error = Error; + + fn try_from(value: Vec) -> Result { + let plugins = value + .into_iter() + .map(|cfg| { + let name = cfg.to_string(); + let plugin: ClientPluginInstance = cfg.try_into()?; + Ok((name, plugin)) + }) + .collect::>>()?; + Ok(Self { plugins }) + } +} + +impl PluginManager { + pub fn get(&self, name: &str) -> Option { + self.plugins.get(name).cloned() + } +} diff --git a/kbs/src/resource/mod.rs b/kbs/src/resource/mod.rs deleted file mode 100644 index 7f5ce4228..000000000 --- a/kbs/src/resource/mod.rs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) 2023 by Alibaba. -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use anyhow::*; -use serde::Deserialize; -use std::fs; -use std::path::Path; -use std::sync::Arc; -use tokio::sync::RwLock; - -mod local_fs; - -#[cfg(feature = "aliyun")] -mod aliyun_kms; - -/// Interface of a `Repository`. -#[async_trait::async_trait] -pub trait Repository { - /// Read secret resource from repository. - async fn read_secret_resource(&self, resource_desc: ResourceDesc) -> Result>; - - /// Write secret resource into repository - async fn write_secret_resource( - &mut self, - resource_desc: ResourceDesc, - data: &[u8], - ) -> Result<()>; -} - -#[derive(Debug, Clone)] -pub struct ResourceDesc { - pub repository_name: String, - pub resource_type: String, - pub resource_tag: String, -} - -impl ResourceDesc { - pub fn is_valid(&self) -> bool { - if &self.repository_name == "." - || &self.repository_name == ".." - || &self.resource_type == "." - || &self.resource_type == ".." - { - return false; - } - true - } -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(tag = "type")] -pub enum RepositoryConfig { - LocalFs(local_fs::LocalFsRepoDesc), - - #[cfg(feature = "aliyun")] - Aliyun(aliyun_kms::AliyunKmsBackendConfig), -} - -impl RepositoryConfig { - pub fn initialize(&self) -> Result>> { - match self { - Self::LocalFs(desc) => { - // Create repository dir. - let dir_path = desc - .dir_path - .clone() - .unwrap_or(local_fs::DEFAULT_REPO_DIR_PATH.to_string()); - - if !Path::new(&dir_path).exists() { - fs::create_dir_all(&dir_path)?; - } - // Create default repo. - if !Path::new(&format!("{}/default", &dir_path)).exists() { - fs::create_dir_all(format!("{}/default", &dir_path))?; - } - - Ok(Arc::new(RwLock::new(local_fs::LocalFs::new(desc)?)) - as Arc>) - } - #[cfg(feature = "aliyun")] - Self::Aliyun(config) => { - let client = aliyun_kms::AliyunKmsBackend::new(config)?; - Ok(Arc::new(RwLock::new(client)) as Arc>) - } - } - } -} - -impl Default for RepositoryConfig { - fn default() -> Self { - Self::LocalFs(local_fs::LocalFsRepoDesc::default()) - } -} - -pub(crate) async fn set_secret_resource( - repository: &Arc>, - resource_desc: ResourceDesc, - data: &[u8], -) -> Result<()> { - repository - .write() - .await - .write_secret_resource(resource_desc, data) - .await -} From 7aad4f9ed592fbdae5ccff80d2d761caba102d96 Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Fri, 27 Sep 2024 15:08:14 +0800 Subject: [PATCH 05/10] KBS: Use new launch Config This is mostly a refactoring patch for KBS. It brings API serving into one function, and will perform different sub-function due to the requested plugin name. This also changes all configuration codes to have a default value. This patch would have some compatibility issue as it changes the old configuration format. The old configuration format is not well classified. This patch tidies the configuration items. Signed-off-by: Xynnn007 --- attestation-service/src/config.rs | 4 +- attestation-service/src/lib.rs | 6 +- attestation-service/src/rvps/mod.rs | 6 +- attestation-service/src/token/mod.rs | 15 +- attestation-service/src/token/simple.rs | 1 - kbs/src/api_server.rs | 317 +++++++++++ kbs/src/attestation/coco/grpc.rs | 41 +- .../attestation/intel_trust_authority/mod.rs | 2 +- kbs/src/bin/kbs.rs | 54 +- kbs/src/config.rs | 490 ++++++++++++++++-- kbs/src/error.rs | 126 +++++ kbs/src/http.rs | 27 + kbs/src/http/config.rs | 131 ----- kbs/src/http/error.rs | 157 ------ kbs/src/http/mod.rs | 41 -- kbs/src/lib.rs | 268 +--------- kbs/src/policy_engine/mod.rs | 6 +- kbs/src/policy_engine/opa/mod.rs | 8 +- kbs/test_data/configs/coco-as-builtin-1.toml | 8 + kbs/test_data/configs/coco-as-builtin-2.toml | 23 + kbs/test_data/configs/coco-as-builtin-3.toml | 28 + kbs/test_data/configs/coco-as-grpc-1.toml | 29 ++ kbs/test_data/configs/coco-as-grpc-2.toml | 12 + kbs/test_data/configs/coco-as-grpc-3.toml | 12 + kbs/test_data/configs/intel-ta-1.toml | 30 ++ kbs/test_data/configs/intel-ta-2.toml | 17 + kbs/test_data/configs/intel-ta-3.toml | 14 + 27 files changed, 1148 insertions(+), 725 deletions(-) create mode 100644 kbs/src/api_server.rs create mode 100644 kbs/src/error.rs create mode 100644 kbs/src/http.rs delete mode 100644 kbs/src/http/config.rs delete mode 100644 kbs/src/http/error.rs delete mode 100644 kbs/src/http/mod.rs create mode 100644 kbs/test_data/configs/coco-as-builtin-1.toml create mode 100644 kbs/test_data/configs/coco-as-builtin-2.toml create mode 100644 kbs/test_data/configs/coco-as-builtin-3.toml create mode 100644 kbs/test_data/configs/coco-as-grpc-1.toml create mode 100644 kbs/test_data/configs/coco-as-grpc-2.toml create mode 100644 kbs/test_data/configs/coco-as-grpc-3.toml create mode 100644 kbs/test_data/configs/intel-ta-1.toml create mode 100644 kbs/test_data/configs/intel-ta-2.toml create mode 100644 kbs/test_data/configs/intel-ta-3.toml diff --git a/attestation-service/src/config.rs b/attestation-service/src/config.rs index e2becb1b6..232567648 100644 --- a/attestation-service/src/config.rs +++ b/attestation-service/src/config.rs @@ -10,7 +10,7 @@ use thiserror::Error; const AS_WORK_DIR: &str = "AS_WORK_DIR"; const DEFAULT_WORK_DIR: &str = "/opt/confidential-containers/attestation-service"; -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq)] pub struct Config { /// The location for Attestation Service to store data. pub work_dir: PathBuf, @@ -19,6 +19,7 @@ pub struct Config { pub policy_engine: String, /// Configurations for RVPS. + #[serde(default)] pub rvps_config: RvpsConfig, /// The Attestation Result Token Broker type. @@ -28,6 +29,7 @@ pub struct Config { pub attestation_token_broker: AttestationTokenBrokerType, /// The Attestation Result Token Broker Config + #[serde(default)] pub attestation_token_config: AttestationTokenConfig, } diff --git a/attestation-service/src/lib.rs b/attestation-service/src/lib.rs index 207bf25ff..6e66c722e 100644 --- a/attestation-service/src/lib.rs +++ b/attestation-service/src/lib.rs @@ -6,9 +6,9 @@ pub mod config; pub mod policy_engine; -mod rvps; -mod token; -mod utils; +pub mod rvps; +pub mod token; +pub mod utils; use crate::token::AttestationTokenBroker; diff --git a/attestation-service/src/rvps/mod.rs b/attestation-service/src/rvps/mod.rs index fe43a7f15..4e34ce847 100644 --- a/attestation-service/src/rvps/mod.rs +++ b/attestation-service/src/rvps/mod.rs @@ -5,7 +5,9 @@ use anyhow::Result; use log::{info, warn}; -use reference_value_provider_service::config::{Config as RvpsCrateConfig, DEFAULT_STORAGE_TYPE}; +pub use reference_value_provider_service::config::{ + Config as RvpsCrateConfig, DEFAULT_STORAGE_TYPE, +}; use serde::Deserialize; use serde_json::{json, Value}; use thiserror::Error; @@ -38,7 +40,7 @@ fn default_store_config() -> Value { json!({}) } -#[derive(Deserialize, Clone, Debug)] +#[derive(Deserialize, Clone, Debug, PartialEq)] pub struct RvpsConfig { /// Address of remote RVPS. If this field is given, a remote RVPS will be connected to. /// If this field is not given, a built-in RVPS will be used. diff --git a/attestation-service/src/token/mod.rs b/attestation-service/src/token/mod.rs index 7212b37e3..1af172196 100644 --- a/attestation-service/src/token/mod.rs +++ b/attestation-service/src/token/mod.rs @@ -6,12 +6,12 @@ use anyhow::*; use serde::Deserialize; use serde_json::Value; -use simple::COCO_AS_ISSUER_NAME; use strum::{Display, EnumString}; mod simple; -const DEFAULT_TOKEN_TIMEOUT: i64 = 5; +pub const COCO_AS_ISSUER_NAME: &str = "CoCo-Attestation-Service"; +pub const DEFAULT_TOKEN_TIMEOUT: i64 = 5; pub trait AttestationTokenBroker { /// Issue an signed attestation token with custom claims. @@ -23,7 +23,7 @@ pub trait AttestationTokenBroker { fn pubkey_jwks(&self) -> Result; } -#[derive(Deserialize, Debug, Clone, EnumString, Display)] +#[derive(Deserialize, Debug, Clone, EnumString, Display, PartialEq)] pub enum AttestationTokenBrokerType { Simple, } @@ -42,9 +42,10 @@ impl AttestationTokenBrokerType { } } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, PartialEq)] pub struct AttestationTokenConfig { /// The Attestation Result Token duration time(in minute) + #[serde(default = "default_duration_min")] pub duration_min: i64, #[serde(default = "default_issuer_name")] @@ -53,11 +54,15 @@ pub struct AttestationTokenConfig { pub signer: Option, } +fn default_duration_min() -> i64 { + DEFAULT_TOKEN_TIMEOUT +} + fn default_issuer_name() -> String { COCO_AS_ISSUER_NAME.to_string() } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, PartialEq)] pub struct TokenSignerConfig { pub key_path: String, pub cert_url: Option, diff --git a/attestation-service/src/token/simple.rs b/attestation-service/src/token/simple.rs index 23281c323..4b20e4ebe 100644 --- a/attestation-service/src/token/simple.rs +++ b/attestation-service/src/token/simple.rs @@ -18,7 +18,6 @@ use serde_json::{json, Value}; use crate::token::{AttestationTokenBroker, AttestationTokenConfig}; -pub const COCO_AS_ISSUER_NAME: &str = "CoCo-Attestation-Service"; const RSA_KEY_BITS: u32 = 2048; const SIMPLE_TOKEN_ALG: &str = "RS384"; diff --git a/kbs/src/api_server.rs b/kbs/src/api_server.rs new file mode 100644 index 000000000..8d4f05772 --- /dev/null +++ b/kbs/src/api_server.rs @@ -0,0 +1,317 @@ +// Copyright (c) 2023 by Rivos Inc. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use actix_web::{ + http::{header::Header, Method}, + middleware, web, App, HttpRequest, HttpResponse, HttpServer, +}; +use actix_web_httpauth::headers::authorization::{Authorization, Bearer}; +use anyhow::Context; +use log::info; + +use crate::{ + admin::Admin, config::KbsConfig, jwe::jwe, plugins::PluginManager, policy_engine::PolicyEngine, + resource::ResourceDesc, token::TokenVerifier, Error, Result, +}; + +const KBS_PREFIX: &str = "/kbs/v0"; + +macro_rules! kbs_path { + ($path:expr) => { + format!("{}/{}", KBS_PREFIX, $path) + }; +} + +/// The KBS API server +#[derive(Clone)] +pub struct ApiServer { + plugin_manager: PluginManager, + + #[cfg(feature = "resource")] + resource_storage: crate::resource::ResourceStorage, + + #[cfg(feature = "as")] + attestation_service: crate::attestation::AttestationService, + + policy_engine: PolicyEngine, + admin_auth: Admin, + config: KbsConfig, + token_verifier: TokenVerifier, +} + +impl ApiServer { + async fn get_attestation_token(&self, request: &HttpRequest) -> anyhow::Result { + #[cfg(feature = "as")] + if let Ok(token) = self + .attestation_service + .get_attest_token_from_session(request) + .await + { + return Ok(token); + } + + let bearer = Authorization::::parse(request) + .context("parse Authorization header failed")? + .into_scheme(); + + let token = bearer.token().to_string(); + + Ok(token) + } + + pub async fn new(config: KbsConfig) -> Result { + let plugin_manager = PluginManager::try_from(config.client_plugins.clone())?; + let token_verifier = TokenVerifier::from_config(config.attestation_token.clone()).await?; + let policy_engine = PolicyEngine::new(&config.policy_engine).await?; + let admin_auth = Admin::try_from(config.admin.clone())?; + + #[cfg(feature = "resource")] + let resource_storage = + crate::resource::ResourceStorage::try_from(config.repository.clone())?; + + #[cfg(feature = "as")] + let attestation_service = + crate::attestation::AttestationService::new(config.attestation_service.clone()).await?; + + Ok(Self { + config, + plugin_manager, + policy_engine, + admin_auth, + token_verifier, + + #[cfg(feature = "resource")] + resource_storage, + + #[cfg(feature = "as")] + attestation_service, + }) + } + + /// Start the HTTP server and serve API requests. + pub async fn serve(self) -> Result<()> { + info!( + "Starting HTTP{} server at {:?}", + if !self.config.http_server.insecure_http { + "S" + } else { + "" + }, + self.config.http_server.sockets + ); + + let http_config = self.config.http_server.clone(); + let http_server = HttpServer::new({ + move || { + let api_server = self.clone(); + App::new() + .wrap(middleware::Logger::default()) + .app_data(web::Data::new(api_server)) + .service( + web::resource([kbs_path!("{plugin}{sub_path:.*}")]) + .route(web::get().to(client)) + .route(web::post().to(client)), + ) + .service( + web::resource([kbs_path!("admin/{plugin}/{sub_path:.*}")]) + .route(web::get().to(admin)) + .route(web::post().to(admin)), + ) + } + }); + + if !http_config.insecure_http { + let tls_server = http_server + .bind_openssl( + &http_config.sockets[..], + crate::http::tls_config(&http_config) + .map_err(|e| Error::HTTPSFailed { source: e })?, + ) + .map_err(|e| Error::HTTPSFailed { source: e.into() })?; + + return tls_server + .run() + .await + .map_err(|e| Error::HTTPSFailed { source: e.into() }); + } + + http_server + .bind(&http_config.sockets[..]) + .map_err(|e| Error::HTTPFailed { source: e.into() })? + .run() + .await + .map_err(|e| Error::HTTPFailed { source: e.into() }) + } +} + +/// Client APIs. /kbs/v0/XXX +pub(crate) async fn client( + request: HttpRequest, + body: web::Bytes, + core: web::Data, +) -> Result { + let query = request.query_string(); + let plugin_name = request + .match_info() + .get("plugin") + .ok_or(Error::IllegalAccessedPath { + path: request.path().to_string(), + })?; + let sub_path = request + .match_info() + .get("sub_path") + .ok_or(Error::IllegalAccessedPath { + path: request.path().to_string(), + })?; + + let end_point = format!("{plugin_name}{sub_path}"); + + match plugin_name { + #[cfg(feature = "as")] + "auth" if request.method() == Method::POST => core + .attestation_service + .auth(&body) + .await + .map_err(From::from), + #[cfg(feature = "as")] + "attest" if request.method() == Method::POST => core + .attestation_service + .attest(&body, request) + .await + .map_err(From::from), + #[cfg(feature = "as")] + "attestation-policy" if request.method() == Method::POST => { + core.admin_auth.validate_auth(&request)?; + + core.attestation_service.set_policy(&body).await?; + + Ok(HttpResponse::Ok().finish()) + } + "resource-policy" if request.method() == Method::POST => { + core.admin_auth.validate_auth(&request)?; + + core.policy_engine.set_policy(&body).await?; + + Ok(HttpResponse::Ok().finish()) + } + #[cfg(feature = "resource")] + "resource" => { + if request.method() == Method::GET { + // Resource APIs needs to be authorized by the Token and policy + let resource_desc = + sub_path + .strip_prefix('/') + .ok_or(Error::IllegalAccessedPath { + path: end_point.clone(), + })?; + + let token = core + .get_attestation_token(&request) + .await + .map_err(|_| Error::TokenNotFound)?; + + let claims = core.token_verifier.verify(token).await?; + + let claim_str = serde_json::to_string(&claims)?; + if !core + .policy_engine + .evaluate(resource_desc, &claim_str) + .await? + { + return Err(Error::PolicyDeny); + }; + + let resource_description = ResourceDesc::try_from(resource_desc)?; + let resource = core + .resource_storage + .get_secret_resource(resource_description) + .await?; + + let public_key = core.token_verifier.extract_tee_public_key(claims)?; + let jwe = jwe(public_key, resource).map_err(|e| Error::JweError { source: e })?; + + let res = serde_json::to_string(&jwe)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(res)) + } else if request.method() == Method::POST { + let resource_desc = + sub_path + .strip_prefix('/') + .ok_or(Error::IllegalAccessedPath { + path: end_point.clone(), + })?; + let resource_description = ResourceDesc::try_from(resource_desc)?; + core.admin_auth.validate_auth(&request)?; + core.resource_storage + .set_secret_resource(resource_description, &body) + .await?; + + Ok(HttpResponse::Ok().content_type("application/json").body("")) + } else { + Ok(HttpResponse::NotImplemented() + .content_type("application/json") + .body("")) + } + } + plugin_name => { + // Plugin calls needs to be authorized by the Token and policy + let token = core + .get_attestation_token(&request) + .await + .map_err(|_| Error::TokenNotFound)?; + + let claims = core.token_verifier.verify(token).await?; + + let claim_str = serde_json::to_string(&claims)?; + + // TODO: add policy filter support for other plugins + if !core.policy_engine.evaluate(&end_point, &claim_str).await? { + return Err(Error::PolicyDeny); + } + + let plugin = core + .plugin_manager + .get(plugin_name) + .ok_or(Error::PluginNotFound { + plugin_name: plugin_name.to_string(), + })?; + let body = body.to_vec(); + let response = plugin + .handle(body, query.into(), sub_path.into(), request.method()) + .await?; + Ok(response) + } + } +} + +/// Admin APIs. +pub(crate) async fn admin( + request: HttpRequest, + _body: web::Bytes, + core: web::Data, +) -> Result { + // Admin APIs needs to be authorized by the admin asymmetric key + core.admin_auth.validate_auth(&request)?; + + let plugin_name = request + .match_info() + .get("plugin") + .ok_or(Error::IllegalAccessedPath { + path: request.path().to_string(), + })?; + let sub_path = request + .match_info() + .get("sub_path") + .ok_or(Error::IllegalAccessedPath { + path: request.path().to_string(), + })?; + + info!("Admin plugin {plugin_name} with path {sub_path} called"); + + // TODO: add admin path handlers + let response = HttpResponse::NotFound().body("no admin plugin found"); + Ok(response) +} diff --git a/kbs/src/attestation/coco/grpc.rs b/kbs/src/attestation/coco/grpc.rs index 7e4d9a3da..064a3ea66 100644 --- a/kbs/src/attestation/coco/grpc.rs +++ b/kbs/src/attestation/coco/grpc.rs @@ -30,17 +30,27 @@ pub const DEFAULT_POOL_SIZE: u64 = 100; pub const COCO_AS_HASH_ALGORITHM: &str = "sha384"; -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq)] pub struct GrpcConfig { - as_addr: Option, - pool_size: Option, + #[serde(default = "default_as_addr")] + pub(crate) as_addr: String, + #[serde(default = "default_pool_size")] + pub(crate) pool_size: u64, +} + +fn default_as_addr() -> String { + DEFAULT_AS_ADDR.to_string() +} + +fn default_pool_size() -> u64 { + DEFAULT_POOL_SIZE } impl Default for GrpcConfig { fn default() -> Self { Self { - as_addr: Some(DEFAULT_AS_ADDR.to_string()), - pool_size: Some(DEFAULT_POOL_SIZE), + as_addr: DEFAULT_AS_ADDR.to_string(), + pool_size: DEFAULT_POOL_SIZE, } } } @@ -51,19 +61,14 @@ pub struct GrpcClientPool { impl GrpcClientPool { pub async fn new(config: GrpcConfig) -> Result { - let as_addr = config.as_addr.unwrap_or_else(|| { - log::info!("Default remote AS address ({DEFAULT_AS_ADDR}) is used"); - DEFAULT_AS_ADDR.to_string() - }); - - let pool_size = config.pool_size.unwrap_or_else(|| { - log::info!("Default AS connection pool size ({DEFAULT_POOL_SIZE}) is used"); - DEFAULT_POOL_SIZE - }); - - info!("connect to remote AS [{as_addr}] with pool size {pool_size}"); - let manager = GrpcManager { as_addr }; - let pool = Mutex::new(Pool::builder().max_open(pool_size).build(manager)); + info!( + "connect to remote AS [{}] with pool size {}", + config.as_addr, config.pool_size + ); + let manager = GrpcManager { + as_addr: config.as_addr, + }; + let pool = Mutex::new(Pool::builder().max_open(config.pool_size).build(manager)); Ok(Self { pool }) } diff --git a/kbs/src/attestation/intel_trust_authority/mod.rs b/kbs/src/attestation/intel_trust_authority/mod.rs index bbfe6f889..9cc9a073d 100644 --- a/kbs/src/attestation/intel_trust_authority/mod.rs +++ b/kbs/src/attestation/intel_trust_authority/mod.rs @@ -76,7 +76,7 @@ struct ErrorResponse { error: String, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq)] pub struct IntelTrustAuthorityConfig { pub base_url: String, pub api_key: String, diff --git a/kbs/src/bin/kbs.rs b/kbs/src/bin/kbs.rs index 5861e3bd3..76725eeaa 100644 --- a/kbs/src/bin/kbs.rs +++ b/kbs/src/bin/kbs.rs @@ -4,19 +4,15 @@ //! Confidential Containers Key Broker Service -extern crate anyhow; - -use anyhow::{bail, Result}; +use anyhow::Result; use std::path::Path; use clap::Parser; -#[cfg(feature = "as")] -use kbs::attestation::AttestationService; use kbs::{ config::{Cli, KbsConfig}, ApiServer, }; -use log::{debug, info, warn}; +use log::{debug, info}; #[tokio::main] async fn main() -> Result<()> { @@ -29,48 +25,8 @@ async fn main() -> Result<()> { debug!("Config: {:#?}", kbs_config); - if !kbs_config.insecure_http - && (kbs_config.private_key.is_none() || kbs_config.certificate.is_none()) - { - bail!("Must specify HTTPS private key and certificate when running in secure mode"); - } - - if kbs_config.insecure_api { - warn!("insecure APIs are enabled"); - } - - #[cfg(feature = "as")] - let attestation_service = { - cfg_if::cfg_if! { - if #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] { - AttestationService::new(kbs_config.as_config.unwrap_or_default()).await? - } else if #[cfg(feature = "coco-as-grpc")] { - AttestationService::new(kbs_config.grpc_config.unwrap_or_default()).await? - } else if #[cfg(feature = "intel-trust-authority-as")] { - AttestationService::new(kbs_config.intel_trust_authority_config).await? - } else { - compile_error!("Please enable at least one of the following features: `coco-as-builtin`, `coco-as-builtin-no-verifier`, `coco-as-grpc` or `intel-trust-authority-as` to continue."); - } - } - }; - - let api_server = ApiServer::new( - kbs_config.sockets, - kbs_config.private_key, - kbs_config.auth_public_key, - kbs_config.certificate, - kbs_config.insecure_http, - #[cfg(feature = "as")] - attestation_service, - kbs_config.timeout, - kbs_config.insecure_api, - #[cfg(feature = "resource")] - kbs_config.repository_config.unwrap_or_default(), - #[cfg(feature = "resource")] - kbs_config.attestation_token_config, - #[cfg(feature = "opa")] - kbs_config.policy_engine_config.unwrap_or_default(), - )?; + let api_server = ApiServer::new(kbs_config).await?; - api_server.serve().await.map_err(anyhow::Error::from) + api_server.serve().await?; + Ok(()) } diff --git a/kbs/src/config.rs b/kbs/src/config.rs index 4359f0687..b145b4af6 100644 --- a/kbs/src/config.rs +++ b/kbs/src/config.rs @@ -2,60 +2,26 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -#[cfg(feature = "coco-as-grpc")] -use crate::attestation::coco::grpc::GrpcConfig; -#[cfg(feature = "intel-trust-authority-as")] -use crate::attestation::intel_trust_authority::IntelTrustAuthorityConfig; -#[cfg(feature = "policy")] +use crate::admin::config::{AdminConfig, DEFAULT_INSECURE_API}; +use crate::plugins::PluginsConfig; use crate::policy_engine::PolicyEngineConfig; -#[cfg(feature = "resource")] -use crate::resource::RepositoryConfig; -#[cfg(feature = "resource")] use crate::token::AttestationTokenVerifierConfig; use anyhow::anyhow; -#[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] -use attestation_service::config::Config as AsConfig; use clap::Parser; use config::{Config, File}; use serde::Deserialize; -use serde_json::Value; use std::net::SocketAddr; use std::path::{Path, PathBuf}; -const DEFAULT_INSECURE_API: bool = false; const DEFAULT_INSECURE_HTTP: bool = false; const DEFAULT_SOCKET: &str = "127.0.0.1:8080"; const DEFAULT_TIMEOUT: i64 = 5; -/// Contains all configurable KBS properties. -#[derive(Clone, Debug, Deserialize)] -pub struct KbsConfig { - /// Resource repository config. - #[cfg(feature = "resource")] - pub repository_config: Option, - - /// Attestation token result broker config. - #[cfg(feature = "resource")] - pub attestation_token_config: AttestationTokenVerifierConfig, - - /// Configuration for the built-in Attestation Service. - #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] - pub as_config: Option, - - /// Configuration for remote attestation over gRPC. - #[cfg(feature = "coco-as-grpc")] - pub grpc_config: Option, - - /// Configuration for Intel Trust Authority attestation. - #[cfg(feature = "intel-trust-authority-as")] - pub intel_trust_authority_config: IntelTrustAuthorityConfig, - +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct HttpServerConfig { /// Socket addresses (IP:port) to listen on, e.g. 127.0.0.1:8080. pub sockets: Vec, - /// HTTPS session timeout in minutes. - pub timeout: i64, - /// HTTPS private key. pub private_key: Option, @@ -65,20 +31,48 @@ pub struct KbsConfig { /// Insecure HTTP. /// WARNING: Using this option makes the HTTP connection insecure. pub insecure_http: bool, +} - /// Public key used to authenticate the resource registration endpoint token (JWT). - /// Only JWTs signed with the corresponding private keys are authenticated. - pub auth_public_key: Option, +impl Default for HttpServerConfig { + fn default() -> Self { + Self { + sockets: vec![DEFAULT_SOCKET.parse().expect("unexpected parse error")], + private_key: None, + certificate: None, + insecure_http: DEFAULT_INSECURE_HTTP, + } + } +} - /// Insecure HTTP APIs. - /// WARNING: Using this option enables KBS insecure APIs such as Resource Registration without - /// verifying the JWK. - pub insecure_api: bool, +/// Contains all configurable KBS properties. +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct KbsConfig { + /// Resource repository config. + #[cfg(feature = "resource")] + #[serde(default)] + pub repository: crate::resource::RepositoryConfig, + + /// Attestation token result broker config. + #[serde(default)] + pub attestation_token: AttestationTokenVerifierConfig, + + /// Configuration for the Attestation Service. + #[cfg(feature = "as")] + pub attestation_service: crate::attestation::config::AttestationConfig, + + /// Configuration for the KBS Http Server + pub http_server: HttpServerConfig, + + /// Configuration for the KBS admin API + pub admin: AdminConfig, /// Policy engine configuration used for evaluating whether the TCB status has access to /// specific resources. - #[cfg(feature = "policy")] - pub policy_engine_config: Option, + #[serde(default)] + pub policy_engine: PolicyEngineConfig, + + #[serde(default)] + pub client_plugins: Vec, } impl TryFrom<&Path> for KbsConfig { @@ -88,10 +82,10 @@ impl TryFrom<&Path> for KbsConfig { /// `config` crate. See `KbsConfig` for schema information. fn try_from(config_path: &Path) -> Result { let c = Config::builder() - .set_default("insecure_api", DEFAULT_INSECURE_API)? - .set_default("insecure_http", DEFAULT_INSECURE_HTTP)? - .set_default("sockets", vec![DEFAULT_SOCKET])? - .set_default("timeout", DEFAULT_TIMEOUT)? + .set_default("admin.insecure_api", DEFAULT_INSECURE_API)? + .set_default("http_server.insecure_http", DEFAULT_INSECURE_HTTP)? + .set_default("http_server.sockets", vec![DEFAULT_SOCKET])? + .set_default("attestation_service.timeout", DEFAULT_TIMEOUT)? .add_source(File::with_name(config_path.to_str().unwrap())) .build()?; @@ -109,3 +103,397 @@ pub struct Cli { #[arg(short, long, env = "KBS_CONFIG_FILE")] pub config_file: String, } + +#[cfg(test)] +mod tests { + use std::path::{Path, PathBuf}; + + use crate::{ + admin::config::AdminConfig, + config::{ + HttpServerConfig, DEFAULT_INSECURE_API, DEFAULT_INSECURE_HTTP, DEFAULT_SOCKET, + DEFAULT_TIMEOUT, + }, + plugins::{sample::SampleConfig, PluginsConfig}, + policy_engine::{PolicyEngineConfig, DEFAULT_POLICY_PATH}, + token::AttestationTokenVerifierConfig, + }; + + use super::KbsConfig; + + #[cfg(feature = "coco-as-builtin")] + use attestation_service::{ + rvps::{RvpsConfig, DEFAULT_STORAGE_TYPE}, + token::{ + AttestationTokenBrokerType, AttestationTokenConfig, COCO_AS_ISSUER_NAME, + DEFAULT_TOKEN_TIMEOUT, + }, + }; + + use rstest::rstest; + use serde_json::json; + + #[rstest] + #[case("test_data/configs/coco-as-grpc-1.toml", KbsConfig { + #[cfg(feature = "resource")] + repository: crate::resource::RepositoryConfig::LocalFs( + crate::resource::local_fs::LocalFsRepoDesc { + dir_path: "/tmp/kbs-resource".into(), + }, + ), + attestation_token: AttestationTokenVerifierConfig { + trusted_certs_paths: vec!["/etc/ca".into(), "/etc/ca2".into()], + insecure_key: false, + trusted_jwk_sets: vec![], + extra_teekey_paths: vec![], + }, + #[cfg(feature = "coco-as-grpc")] + attestation_service: crate::attestation::config::AttestationConfig { + attestation_service: + crate::attestation::config::AttestationServiceConfig::CoCoASGrpc( + crate::attestation::coco::grpc::GrpcConfig { + as_addr: "http://127.0.0.1:50001".into(), + pool_size: 100, + }, + ), + timeout: 600, + }, + http_server: HttpServerConfig { + sockets: vec!["0.0.0.0:8080".parse().unwrap()], + private_key: Some("/etc/kbs-private.key".into()), + certificate: Some("/etc/kbs-cert.pem".into()), + insecure_http: false, + }, + admin: AdminConfig { + auth_public_key: Some(PathBuf::from("/etc/kbs-admin.pub")), + insecure_api: false, + }, + policy_engine: PolicyEngineConfig { + policy_path: PathBuf::from("/etc/kbs-policy.rego"), + }, + client_plugins: vec![PluginsConfig::Sample(SampleConfig { + item: "value1".into(), + })], + })] + #[case("test_data/configs/coco-as-builtin-1.toml", KbsConfig { + #[cfg(feature = "resource")] + repository: crate::resource::RepositoryConfig::LocalFs( + crate::resource::local_fs::LocalFsRepoDesc { + dir_path: DEFAULT_REPO_DIR_PATH.into(), + }, + ), + attestation_token: AttestationTokenVerifierConfig { + trusted_certs_paths: vec![], + insecure_key: false, + trusted_jwk_sets: vec![], + extra_teekey_paths: vec![], + }, + #[cfg(feature = "coco-as-builtin")] + attestation_service: crate::attestation::config::AttestationConfig { + attestation_service: + crate::attestation::config::AttestationServiceConfig::CoCoASBuiltIn( + attestation_service::config::Config { + work_dir: "/opt/coco/attestation-service".into(), + policy_engine: "opa".into(), + attestation_token_broker: AttestationTokenBrokerType::Simple, + rvps_config: RvpsConfig { + remote_addr: "http://127.0.0.1:50003".into(), + store_type: DEFAULT_STORAGE_TYPE.into(), + store_config: json!({}), + }, + attestation_token_config: AttestationTokenConfig { + duration_min: DEFAULT_TOKEN_TIMEOUT, + issuer_name: COCO_AS_ISSUER_NAME.into(), + signer: None, + }, + } + ), + timeout: DEFAULT_TIMEOUT, + }, + http_server: HttpServerConfig { + sockets: vec![DEFAULT_SOCKET.parse().unwrap()], + private_key: None, + certificate: None, + insecure_http: DEFAULT_INSECURE_HTTP, + }, + admin: AdminConfig { + auth_public_key: None, + insecure_api: DEFAULT_INSECURE_API, + }, + policy_engine: PolicyEngineConfig { + policy_path: DEFAULT_POLICY_PATH.into(), + }, + client_plugins: vec![], + })] + #[case("test_data/configs/intel-ta-1.toml", KbsConfig { + #[cfg(feature = "resource")] + repository: crate::resource::RepositoryConfig::LocalFs( + crate::resource::local_fs::LocalFsRepoDesc { + dir_path: "/tmp/kbs-resource".into(), + }, + ), + attestation_token: AttestationTokenVerifierConfig { + trusted_jwk_sets: vec!["/etc/ca".into(), "/etc/ca2".into()], + insecure_key: false, + trusted_certs_paths: vec![], + extra_teekey_paths: vec![], + }, + #[cfg(feature = "intel-trust-authority-as")] + attestation_service: crate::attestation::config::AttestationConfig { + attestation_service: + crate::attestation::config::AttestationServiceConfig::IntelTA( + crate::attestation::intel_trust_authority::IntelTrustAuthorityConfig { + base_url: "example.io".into(), + api_key: "this-is-a-key".into(), + certs_file: "file:///etc/ita-cert.pem".into(), + allow_unmatched_policy: Some(true), + } + ), + timeout: DEFAULT_TIMEOUT, + }, + http_server: HttpServerConfig { + sockets: vec!["0.0.0.0:8080".parse().unwrap()], + private_key: Some("/etc/kbs-private.key".into()), + certificate: Some("/etc/kbs-cert.pem".into()), + insecure_http: false, + }, + admin: AdminConfig { + auth_public_key: Some(PathBuf::from("/etc/kbs-admin.pub")), + insecure_api: false, + }, + policy_engine: PolicyEngineConfig { + policy_path: PathBuf::from("/etc/kbs-policy.rego"), + }, + client_plugins: vec![PluginsConfig::Sample(SampleConfig { + item: "value1".into(), + })], + })] + #[case("test_data/configs/coco-as-grpc-2.toml", KbsConfig { + #[cfg(feature = "resource")] + repository: crate::resource::RepositoryConfig::default(), + attestation_token: AttestationTokenVerifierConfig { + ..Default::default() + }, + #[cfg(feature = "coco-as-grpc")] + attestation_service: crate::attestation::config::AttestationConfig { + attestation_service: + crate::attestation::config::AttestationServiceConfig::CoCoASGrpc( + crate::attestation::coco::grpc::GrpcConfig { + as_addr: "http://as:50004".into(), + pool_size: crate::attestation::coco::grpc::DEFAULT_POOL_SIZE, + }, + ), + timeout: DEFAULT_TIMEOUT, + }, + http_server: HttpServerConfig { + sockets: vec!["0.0.0.0:8080".parse().unwrap()], + private_key: None, + certificate: None, + insecure_http: true, + }, + admin: AdminConfig { + auth_public_key: Some(PathBuf::from("/opt/confidential-containers/kbs/user-keys/public.pub")), + insecure_api: DEFAULT_INSECURE_API, + }, + policy_engine: PolicyEngineConfig::default(), + client_plugins: Vec::default(), + })] + #[case("test_data/configs/coco-as-builtin-2.toml", KbsConfig { + #[cfg(feature = "resource")] + repository: crate::resource::RepositoryConfig::LocalFs( + crate::resource::local_fs::LocalFsRepoDesc { + dir_path: DEFAULT_REPO_DIR_PATH.into(), + }, + ), + attestation_token: AttestationTokenVerifierConfig { + trusted_certs_paths: vec![], + insecure_key: false, + trusted_jwk_sets: vec![], + extra_teekey_paths: vec![], + }, + #[cfg(feature = "coco-as-builtin")] + attestation_service: crate::attestation::config::AttestationConfig { + attestation_service: + crate::attestation::config::AttestationServiceConfig::CoCoASBuiltIn( + attestation_service::config::Config { + work_dir: "/opt/confidential-containers/attestation-service".into(), + policy_engine: "opa".into(), + attestation_token_broker: AttestationTokenBrokerType::Simple, + rvps_config: RvpsConfig { + remote_addr: "".into(), + store_type: "LocalFs".into(), + store_config: json!({}), + }, + attestation_token_config: AttestationTokenConfig { + duration_min: 5, + ..Default::default() + }, + } + ), + timeout: DEFAULT_TIMEOUT, + }, + http_server: HttpServerConfig { + sockets: vec!["0.0.0.0:8080".parse().unwrap()], + private_key: None, + certificate: None, + insecure_http: true, + }, + admin: AdminConfig { + auth_public_key: Some("/kbs/kbs.pem".into()), + insecure_api: DEFAULT_INSECURE_API, + }, + policy_engine: PolicyEngineConfig::default(), + client_plugins: vec![], + })] + #[case("test_data/configs/intel-ta-2.toml", KbsConfig { + attestation_token: AttestationTokenVerifierConfig { + trusted_jwk_sets: vec!["https://portal.trustauthority.intel.com".into()], + insecure_key: false, + trusted_certs_paths: vec![], + extra_teekey_paths: vec![], + }, + #[cfg(feature = "intel-trust-authority-as")] + attestation_service: crate::attestation::config::AttestationConfig { + attestation_service: + crate::attestation::config::AttestationServiceConfig::IntelTA( + crate::attestation::intel_trust_authority::IntelTrustAuthorityConfig { + base_url: "https://api.trustauthority.intel.com".into(), + api_key: "tBfd5kKX2x9ahbodKV1...".into(), + certs_file: "https://portal.trustauthority.intel.com".into(), + allow_unmatched_policy: None, + } + ), + timeout: DEFAULT_TIMEOUT, + }, + http_server: HttpServerConfig { + sockets: vec!["0.0.0.0:8080".parse().unwrap()], + private_key: None, + certificate: None, + insecure_http: true, + }, + admin: AdminConfig { + auth_public_key: Some("/kbs/kbs.pem".into()), + insecure_api: DEFAULT_INSECURE_API, + }, + policy_engine: PolicyEngineConfig::default(), + client_plugins: vec![], + #[cfg(feature = "resource")] + repository: crate::resource::RepositoryConfig::LocalFs( + crate::resource::local_fs::LocalFsRepoDesc::default(), + ), + })] + #[case("test_data/configs/coco-as-grpc-3.toml", KbsConfig { + #[cfg(feature = "resource")] + repository: crate::resource::RepositoryConfig::default(), + attestation_token: AttestationTokenVerifierConfig { + ..Default::default() + }, + #[cfg(feature = "coco-as-grpc")] + attestation_service: crate::attestation::config::AttestationConfig { + attestation_service: + crate::attestation::config::AttestationServiceConfig::CoCoASGrpc( + crate::attestation::coco::grpc::GrpcConfig { + as_addr: "http://127.0.0.1:50004".into(), + pool_size: 100, + }, + ), + timeout: DEFAULT_TIMEOUT, + }, + http_server: HttpServerConfig { + insecure_http: true, + ..Default::default() + }, + admin: AdminConfig { + insecure_api: true, + ..Default::default() + }, + policy_engine: PolicyEngineConfig::default(), + client_plugins: Vec::default(), + })] + #[case("test_data/configs/intel-ta-3.toml", KbsConfig { + attestation_token: AttestationTokenVerifierConfig { + trusted_jwk_sets: vec!["https://portal.trustauthority.intel.com".into()], + insecure_key: false, + trusted_certs_paths: vec![], + extra_teekey_paths: vec![], + }, + #[cfg(feature = "intel-trust-authority-as")] + attestation_service: crate::attestation::config::AttestationConfig { + attestation_service: + crate::attestation::config::AttestationServiceConfig::IntelTA( + crate::attestation::intel_trust_authority::IntelTrustAuthorityConfig { + base_url: "https://api.trustauthority.intel.com".into(), + api_key: "tBfd5kKX2x9ahbodKV1...".into(), + certs_file: "https://portal.trustauthority.intel.com".into(), + allow_unmatched_policy: None, + } + ), + timeout: DEFAULT_TIMEOUT, + }, + http_server: HttpServerConfig { + insecure_http: true, + ..Default::default() + }, + admin: AdminConfig { + insecure_api: true, + ..Default::default() + }, + policy_engine: PolicyEngineConfig::default(), + client_plugins: vec![], + #[cfg(feature = "resource")] + repository: crate::resource::RepositoryConfig::LocalFs( + crate::resource::local_fs::LocalFsRepoDesc::default(), + ), + })] + #[case("test_data/configs/coco-as-builtin-3.toml", KbsConfig { + #[cfg(feature = "resource")] + repository: crate::resource::RepositoryConfig::LocalFs( + crate::resource::local_fs::LocalFsRepoDesc { + dir_path: "/opt/confidential-containers/kbs/repository".into(), + }, + ), + attestation_token: AttestationTokenVerifierConfig { + trusted_certs_paths: vec![], + insecure_key: false, + trusted_jwk_sets: vec![], + extra_teekey_paths: vec![], + }, + #[cfg(feature = "coco-as-builtin")] + attestation_service: crate::attestation::config::AttestationConfig { + attestation_service: + crate::attestation::config::AttestationServiceConfig::CoCoASBuiltIn( + attestation_service::config::Config { + work_dir: "/opt/confidential-containers/attestation-service".into(), + policy_engine: "opa".into(), + attestation_token_broker: AttestationTokenBrokerType::Simple, + rvps_config: RvpsConfig { + remote_addr: "".into(), + store_type: "LocalFs".into(), + ..Default::default() + }, + attestation_token_config: AttestationTokenConfig { + duration_min: 5, + ..Default::default() + }, + } + ), + timeout: DEFAULT_TIMEOUT, + }, + http_server: HttpServerConfig { + insecure_http: true, + ..Default::default() + }, + admin: AdminConfig { + insecure_api: true, + ..Default::default() + }, + policy_engine: PolicyEngineConfig { + policy_path: "/opa/confidential-containers/kbs/policy.rego".into(), + }, + client_plugins: vec![], + })] + fn read_config(#[case] config_path: &str, #[case] expected: KbsConfig) { + let config = KbsConfig::try_from(Path::new(config_path)).unwrap(); + assert_eq!(config, expected, "case {config_path}"); + } +} diff --git a/kbs/src/error.rs b/kbs/src/error.rs new file mode 100644 index 000000000..5cdaf6886 --- /dev/null +++ b/kbs/src/error.rs @@ -0,0 +1,126 @@ +// Copyright (c) 2023 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +//! This Error type helps to work with Actix-web + +use std::fmt::Write; + +use actix_web::{body::BoxBody, HttpResponse, ResponseError}; +use kbs_types::ErrorInformation; +use log::error; +use strum::AsRefStr; +use thiserror::Error; + +const ERROR_TYPE_PREFIX: &str = "https://github.com/confidential-containers/kbs/errors"; + +pub type Result = std::result::Result; + +#[derive(Error, AsRefStr, Debug)] +pub enum Error { + #[error("Admin auth error")] + AdminAuth(#[from] crate::admin::Error), + + #[cfg(feature = "as")] + #[error("Attestation error")] + AttestationError(#[from] crate::attestation::Error), + + #[error("HTTP initialization failed")] + HTTPFailed { + #[source] + source: anyhow::Error, + }, + + #[error("HTTPS initialization failed")] + HTTPSFailed { + #[source] + source: anyhow::Error, + }, + + #[error("Accessed path {path} is illegal")] + IllegalAccessedPath { path: String }, + + #[error("JWE failed")] + JweError { + #[source] + source: anyhow::Error, + }, + + #[error("PluginManager initialization failed")] + PluginManagerInitialization { + #[source] + source: anyhow::Error, + }, + + #[error("Plugin {plugin_name} not found")] + PluginNotFound { plugin_name: String }, + + #[error("Plugin internal error")] + PluginInternalError { + #[source] + source: anyhow::Error, + }, + + #[error("Access denied by policy")] + PolicyDeny, + + #[error("Policy engine error")] + PolicyEngine(#[from] crate::policy_engine::KbsPolicyEngineError), + + #[cfg(feature = "resource")] + #[error("Resource access failed")] + ResourceAccessFailed(#[from] crate::resource::Error), + + #[error("Serialize/Deserialize failed")] + SerdeError(#[from] serde_json::Error), + + #[error("Attestation Token not found")] + TokenNotFound, + + #[error("Token Verifier error")] + TokenVerifierError(#[from] crate::token::Error), +} + +impl ResponseError for Error { + fn error_response(&self) -> HttpResponse { + let mut detail = String::new(); + + // The write macro here will only raise error when OOM of the string. + write!(&mut detail, "{}", self).expect("written error response failed"); + let info = ErrorInformation { + error_type: format!("{ERROR_TYPE_PREFIX}/{}", self.as_ref()), + detail, + }; + + // All the fields inside the ErrorInfo are printable characters, so this + // error cannot happen. + // A test covering all the possible error types are given to ensure this. + let body = serde_json::to_string(&info).expect("serialize error response failed"); + + // Due to the definition of KBS attestation protocol, we set the http code. + let mut res = match self { + Error::IllegalAccessedPath { .. } | Error::PluginNotFound { .. } => { + HttpResponse::NotFound() + } + _ => HttpResponse::Unauthorized(), + }; + + error!("{self:?}"); + + res.body(BoxBody::new(body)) + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::Error; + + #[rstest] + #[case(Error::IllegalAccessedPath{path: "test".into()})] + #[case(Error::PluginNotFound{plugin_name: "test".into()})] + fn into_error_response(#[case] err: Error) { + let _ = actix_web::ResponseError::error_response(&err); + } +} diff --git a/kbs/src/http.rs b/kbs/src/http.rs new file mode 100644 index 000000000..9cc929c96 --- /dev/null +++ b/kbs/src/http.rs @@ -0,0 +1,27 @@ +// Copyright (c) 2024 by Alibaba. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{anyhow, Result}; + +use crate::config::HttpServerConfig; + +pub fn tls_config(config: &HttpServerConfig) -> Result { + use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; + + let cert_file = config + .certificate + .as_ref() + .ok_or_else(|| anyhow!("Missing certificate"))?; + + let key_file = config + .private_key + .as_ref() + .ok_or_else(|| anyhow!("Missing private key"))?; + + let mut builder = SslAcceptor::mozilla_modern(SslMethod::tls())?; + builder.set_private_key_file(key_file, SslFiletype::PEM)?; + builder.set_certificate_chain_file(cert_file)?; + + Ok(builder) +} diff --git a/kbs/src/http/config.rs b/kbs/src/http/config.rs deleted file mode 100644 index 8328a380b..000000000 --- a/kbs/src/http/config.rs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) 2023 by Alibaba. -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use super::*; - -#[cfg(feature = "as")] -#[derive(serde::Deserialize, Debug)] -pub struct SetPolicyInput { - policy_id: String, - policy: String, -} - -#[cfg(feature = "as")] -/// POST /attestation-policy -pub(crate) async fn attestation_policy( - request: HttpRequest, - input: web::Json, - user_pub_key: web::Data>, - insecure: web::Data, - attestation_service: web::Data>, -) -> Result { - if !insecure.get_ref() { - let user_pub_key = user_pub_key - .as_ref() - .as_ref() - .ok_or(Error::UserPublicKeyNotProvided)?; - - validate_auth(&request, user_pub_key).map_err(|e| { - Error::FailedAuthentication(format!("Requester is not an authorized user: {e}")) - })?; - } - - attestation_service - .set_policy(&input.policy_id, &input.policy) - .await - .map_err(|e| Error::PolicyEndpoint(format!("Set policy error {e}")))?; - - Ok(HttpResponse::Ok().finish()) -} - -#[cfg(feature = "policy")] -/// POST /resource-policy -pub(crate) async fn resource_policy( - request: HttpRequest, - input: web::Json, - user_pub_key: web::Data>, - insecure: web::Data, - policy_engine: web::Data, -) -> Result { - if !insecure.get_ref() { - let user_pub_key = user_pub_key - .as_ref() - .as_ref() - .ok_or(Error::UserPublicKeyNotProvided)?; - - validate_auth(&request, user_pub_key).map_err(|e| { - Error::FailedAuthentication(format!("Requester is not an authorized user: {e}")) - })?; - } - - policy_engine - .0 - .lock() - .await - .set_policy( - input.into_inner()["policy"] - .as_str() - .ok_or(Error::PolicyEndpoint( - "Get policy from request failed".to_string(), - ))? - .to_string(), - ) - .await - .map_err(|e| Error::PolicyEndpoint(format!("Set policy error {e}")))?; - - Ok(HttpResponse::Ok().finish()) -} - -#[cfg(feature = "resource")] -/// POST /resource/{repository}/{type}/{tag} -/// POST /resource/{type}/{tag} -/// -/// TODO: Although this endpoint is authenticated through a JSON Web Token (JWT), -/// only identified users should be able to get a JWT and access it. -/// At the moment user identification is not supported, and the KBS CLI -/// `--user-public-key` defines the authorized user for that endpoint. In other words, -/// any JWT signed with the user's private key will be authenticated. -/// JWT generation and user identification is unimplemented for now, and thus this -/// endpoint is insecure and is only meant for testing purposes. -pub(crate) async fn set_resource( - request: HttpRequest, - data: web::Bytes, - user_pub_key: web::Data>, - insecure: web::Data, - repository: web::Data>>, -) -> Result { - if !insecure.get_ref() { - let user_pub_key = user_pub_key - .as_ref() - .as_ref() - .ok_or(Error::UserPublicKeyNotProvided)?; - - validate_auth(&request, user_pub_key).map_err(|e| { - Error::FailedAuthentication(format!("Requester is not an authorized user: {e}")) - })?; - } - - let resource_description = ResourceDesc { - repository_name: request - .match_info() - .get("repository") - .unwrap_or("default") - .to_string(), - resource_type: request - .match_info() - .get("type") - .ok_or_else(|| Error::InvalidRequest(String::from("no `type` in url")))? - .to_string(), - resource_tag: request - .match_info() - .get("tag") - .ok_or_else(|| Error::InvalidRequest(String::from("no `tag` in url")))? - .to_string(), - }; - - set_secret_resource(&repository, resource_description, data.as_ref()) - .await - .map_err(|e| Error::SetSecretFailed(format!("{e}")))?; - Ok(HttpResponse::Ok().content_type("application/json").body("")) -} diff --git a/kbs/src/http/error.rs b/kbs/src/http/error.rs deleted file mode 100644 index 70fd46128..000000000 --- a/kbs/src/http/error.rs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) 2023 by Alibaba. -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -//! This Error type helps to work with Actix-web - -use std::fmt::{Display, Write}; - -use actix_web::{ - body::BoxBody, - http::header::{self, TryIntoHeaderValue}, - web::BytesMut, - HttpResponse, Responder, ResponseError, -}; -use kbs_types::ErrorInformation; -use log::error; -use serde::Serialize; -use strum::AsRefStr; -use thiserror::Error; - -const ERROR_TYPE_PREFIX: &str = "https://github.com/confidential-containers/kbs/errors"; - -pub type Result = std::result::Result; - -#[allow(dead_code)] -#[derive(Error, AsRefStr, Debug)] -pub enum Error { - #[error("Attestation failed: {0}")] - AttestationFailed(String), - - #[error("Received illegal attestation claims: {0}")] - AttestationClaimsParseFailed(String), - - #[error("The cookie is expired")] - ExpiredCookie, - - #[error("Authentication failed: {0}")] - FailedAuthentication(String), - - #[error("The cookie is invalid")] - InvalidCookie, - - #[error("The request is invalid: {0}")] - InvalidRequest(String), - - #[error("Json Web Encryption failed: {0}")] - JWEFailed(String), - - #[error("The cookie is missing")] - MissingCookie, - - #[error("Policy error: {0}")] - PolicyEndpoint(String), - - #[error("Resource policy engine evaluate failed: {0}")] - PolicyEngineFailed(String), - - #[error("Resource not permitted.")] - PolicyReject, - - #[error("KBS Client Protocol Version Mismatch: {0}")] - ProtocolVersion(String), - - #[error("Public key get failed: {0}")] - PublicKeyGetFailed(String), - - #[error("Read secret failed: {0}")] - ReadSecretFailed(String), - - #[error("Set secret failed: {0}")] - SetSecretFailed(String), - - #[error("Attestation token issue failed: {0}")] - TokenIssueFailed(String), - - #[error("Received an illegal token: {0}")] - TokenParseFailed(String), - - #[error("The cookie is unauthenticated")] - UnAuthenticatedCookie, - - #[error("User public key not provided when launching the KBS")] - UserPublicKeyNotProvided, -} - -/// For example, if we want to raise an error of `MissingCookie` -/// ```ignore -/// raise_error!(Error::MissingCookie); -/// ``` -/// is short for -/// ```ignore -/// return Err(Error::MissingCookie); -/// ``` -#[macro_export] -macro_rules! raise_error { - ($error: expr) => { - return Err($error) - }; -} - -impl ResponseError for Error { - fn error_response(&self) -> HttpResponse { - let mut detail = String::new(); - - // The write macro here will only raise error when OOM of the string. - write!(&mut detail, "{}", self).expect("written error response failed"); - let info = ErrorInformation { - error_type: format!("{ERROR_TYPE_PREFIX}/{}", self.as_ref()), - detail, - }; - - // All the fields inside the ErrorInfo are printable characters, so this - // error cannot happen. - // A test covering all the possible error types are given to ensure this. - let body = serde_json::to_string(&info).expect("serialize error response failed"); - - // Due to the definition of KBS attestation protocol, we set the http code. - let mut res = match self { - Error::ReadSecretFailed(_) => HttpResponse::NotFound(), - _ => HttpResponse::Unauthorized(), - }; - - error!("{self}"); - - res.body(BoxBody::new(body)) - } -} - -#[cfg(test)] -mod tests { - use rstest::rstest; - - use crate::http::Error; - - #[rstest] - #[case(Error::AttestationFailed("test".into()))] - #[case(Error::ExpiredCookie)] - #[case(Error::FailedAuthentication("test".into()))] - #[case(Error::InvalidCookie)] - #[case(Error::ExpiredCookie)] - #[case(Error::MissingCookie)] - #[case(Error::InvalidRequest("test".into()))] - #[case(Error::JWEFailed("test".into()))] - #[case(Error::PolicyEndpoint("test".into()))] - #[case(Error::PolicyReject)] - #[case(Error::ProtocolVersion("test".into()))] - #[case(Error::PublicKeyGetFailed("test".into()))] - #[case(Error::ReadSecretFailed("test".into()))] - #[case(Error::SetSecretFailed("test".into()))] - #[case(Error::TokenIssueFailed("test".into()))] - #[case(Error::TokenParseFailed("test".into()))] - #[case(Error::UnAuthenticatedCookie)] - #[case(Error::UserPublicKeyNotProvided)] - fn into_error_response(#[case] err: Error) { - let _ = actix_web::ResponseError::error_response(&err); - } -} diff --git a/kbs/src/http/mod.rs b/kbs/src/http/mod.rs deleted file mode 100644 index 4d4c44323..000000000 --- a/kbs/src/http/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2022 by Rivos Inc. -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(feature = "as")] -use crate::attestation::{AttestationService, AS_TOKEN_TEE_PUBKEY_PATH}; -use crate::auth::validate_auth; -#[cfg(feature = "policy")] -use crate::policy_engine::PolicyEngine; -#[cfg(feature = "resource")] -use crate::resource::{set_secret_resource, Repository, ResourceDesc}; -#[cfg(feature = "as")] -use crate::session::{SessionMap, KBS_SESSION_ID}; -use actix_web::Responder; -use actix_web::{body::BoxBody, web, HttpRequest, HttpResponse}; -use jwt_simple::prelude::Ed25519PublicKey; -use kbs_types::{Attestation, Challenge, ErrorInformation, Request}; -use std::sync::Arc; -use tokio::sync::{Mutex, RwLock}; - -#[cfg(feature = "as")] -mod attest; - -mod config; -mod error; - -#[cfg(feature = "resource")] -mod resource; - -#[cfg(feature = "as")] -/// RESTful APIs that related to attestation -pub use attest::*; - -/// RESTful APIs that configure KBS and AS, require user authentication -pub use self::config::*; - -#[cfg(feature = "resource")] -/// RESTful APIs that to get secret resources, need attestation verification -pub use resource::*; - -pub use error::*; diff --git a/kbs/src/lib.rs b/kbs/src/lib.rs index 1bc7ed052..d11e0edcd 100644 --- a/kbs/src/lib.rs +++ b/kbs/src/lib.rs @@ -4,273 +4,27 @@ //! KBS API server -#![allow(clippy::too_many_arguments)] - -extern crate actix_web; -extern crate anyhow; -extern crate base64; -extern crate env_logger; -extern crate kbs_types; -#[macro_use] -extern crate lazy_static; -extern crate log; -extern crate rand; -extern crate uuid; - -use actix_web::{middleware, web, App, HttpServer}; -use anyhow::{anyhow, bail, Context, Result}; -#[cfg(feature = "as")] -use attestation::AttestationService; -use jwt_simple::prelude::Ed25519PublicKey; -use openssl::ssl::SslAcceptorBuilder; -#[cfg(feature = "resource")] -use resource::RepositoryConfig; -#[cfg(feature = "as")] -use std::sync::Arc; -use std::{net::SocketAddr, path::PathBuf}; -#[cfg(feature = "resource")] -use token::AttestationTokenVerifierConfig; - -#[cfg(feature = "as")] -use crate::session::SessionMap; - -#[cfg(feature = "policy")] -use crate::policy_engine::{PolicyEngine, PolicyEngineConfig}; - #[cfg(feature = "as")] -/// Attestation Service pub mod attestation; -#[allow(unused_imports)] -/// KBS config -pub mod config; - -mod auth; -#[allow(unused_imports)] -mod http; - #[cfg(feature = "resource")] mod resource; -#[cfg(feature = "as")] -mod session; - -#[cfg(feature = "resource")] +/// KBS config +pub mod config; +pub use config::KbsConfig; mod token; -#[cfg(feature = "policy")] /// Resource Policy Engine pub mod policy_engine; -static KBS_PREFIX: &str = "/kbs/v0"; - -macro_rules! kbs_path { - ($path:expr) => { - format!("{}/{}", KBS_PREFIX, $path) - }; -} - -#[allow(dead_code)] -/// The KBS API server -pub struct ApiServer { - sockets: Vec, - private_key: Option, - /// This user public key is used to verify the jwt. - /// The jwt is carried with the POST request for - /// resource registration - user_public_key: Option, - certificate: Option, - insecure: bool, - - #[cfg(feature = "as")] - attestation_service: Arc, - - http_timeout: i64, - insecure_api: bool, - #[cfg(feature = "resource")] - repository_config: RepositoryConfig, - #[cfg(feature = "resource")] - attestation_token_config: AttestationTokenVerifierConfig, - #[cfg(feature = "policy")] - policy_engine_config: PolicyEngineConfig, -} - -impl ApiServer { - /// Create a new KBS HTTP server - pub fn new( - sockets: Vec, - private_key: Option, - user_public_key: Option, - certificate: Option, - insecure: bool, - - #[cfg(feature = "as")] attestation_service: AttestationService, - - http_timeout: i64, - insecure_api: bool, - #[cfg(feature = "resource")] repository_config: RepositoryConfig, - #[cfg(feature = "resource")] attestation_token_config: AttestationTokenVerifierConfig, - #[cfg(feature = "policy")] policy_engine_config: PolicyEngineConfig, - ) -> Result { - if !insecure && (private_key.is_none() || certificate.is_none()) { - bail!("Missing HTTPS credentials"); - } - - cfg_if::cfg_if! { - if #[cfg(not(any(feature = "as", feature = "resource")))] { - compile_error!("Must enable at least one of the following features: `as`, `resource`"); - } - } - - Ok(ApiServer { - sockets, - private_key, - user_public_key, - certificate, - insecure, - - #[cfg(feature = "as")] - attestation_service: Arc::new(attestation_service), - - http_timeout, - insecure_api, - #[cfg(feature = "resource")] - repository_config, - #[cfg(feature = "resource")] - attestation_token_config, - #[cfg(feature = "policy")] - policy_engine_config, - }) - } - - fn tls_config(&self) -> Result { - use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; - - let cert_file = self - .certificate - .clone() - .ok_or_else(|| anyhow!("Missing certificate"))?; - - let key_file = self - .private_key - .clone() - .ok_or_else(|| anyhow!("Missing private key"))?; - - let mut builder = SslAcceptor::mozilla_modern(SslMethod::tls())?; - builder.set_private_key_file(key_file, SslFiletype::PEM)?; - builder.set_certificate_chain_file(cert_file)?; - - Ok(builder) - } - - /// Start the HTTP server and serve API requests. - pub async fn serve(&self) -> Result<()> { - log::info!( - "Starting HTTP{} server at {:?}", - if !self.insecure { "S" } else { "" }, - self.sockets - ); - - #[cfg(feature = "as")] - let (attestation_service, sessions) = { - let attestation_service = web::Data::new(self.attestation_service.clone()); - let sessions = web::Data::new(SessionMap::new()); - let sessions_clone = sessions.clone(); - - tokio::spawn(async move { - loop { - tokio::time::sleep(std::time::Duration::from_secs(60)).await; - sessions_clone - .sessions - .retain_async(|_, v| !v.is_expired()) - .await; - } - }); - (attestation_service, sessions) - }; - - let http_timeout = self.http_timeout; - - #[cfg(feature = "resource")] - let repository = self.repository_config.initialize()?; - - #[cfg(feature = "resource")] - let token_verifier = - crate::token::TokenVerifier::from_config(self.attestation_token_config.clone()).await?; - - #[cfg(feature = "policy")] - let policy_engine = PolicyEngine::new(&self.policy_engine_config).await?; - - let user_public_key = match self.insecure_api { - true => None, - false => match &self.user_public_key { - Some(key_path) => { - let user_public_key_pem = tokio::fs::read_to_string(key_path) - .await - .context("read user public key")?; - let key = Ed25519PublicKey::from_pem(&user_public_key_pem) - .context("parse user public key")?; - Some(key) - } - None => bail!("no user public key given"), - }, - }; - - let insecure_api = self.insecure_api; - - let http_server = HttpServer::new(move || { - #[allow(unused_mut)] - let mut server_app = App::new() - .wrap(middleware::Logger::default()) - .app_data(web::Data::new(http_timeout)) - .app_data(web::Data::new(user_public_key.clone())) - .app_data(web::Data::new(insecure_api)); - - cfg_if::cfg_if! { - if #[cfg(feature = "as")] { - server_app = server_app.app_data(web::Data::clone(&sessions)) - .app_data(web::Data::clone(&attestation_service)).service(web::resource(kbs_path!("auth")).route(web::post().to(http::auth))) - .service(web::resource(kbs_path!("attest")).route(web::post().to(http::attest))) - .service( - web::resource(kbs_path!("attestation-policy")) - .route(web::post().to(http::attestation_policy)), - ); - }} - cfg_if::cfg_if! { - if #[cfg(feature = "resource")] { - server_app = server_app.app_data(web::Data::new(repository.clone())) - .app_data(web::Data::new(token_verifier.clone())) - .service( - web::resource([ - kbs_path!("resource/{repository}/{type}/{tag}"), - kbs_path!("resource/{type}/{tag}"), - ]) - .route(web::get().to(http::get_resource)) - .route(web::post().to(http::set_resource)), - ); - } - } - cfg_if::cfg_if! { - if #[cfg(feature = "policy")] { - server_app = server_app.app_data(web::Data::new(policy_engine.clone())) - .service( - web::resource(kbs_path!("resource-policy")).route(web::post().to(http::resource_policy)), - ); - } - } - server_app - }); +pub mod api_server; +pub use api_server::ApiServer; - if !self.insecure { - let tls_server = http_server.bind_openssl(&self.sockets[..], self.tls_config()?)?; +pub mod error; +pub mod plugins; +pub use error::*; - tls_server.run().await.map_err(anyhow::Error::from) - } else { - http_server - .bind(&self.sockets[..])? - .run() - .await - .map_err(anyhow::Error::from) - } - } -} +pub mod admin; +pub mod http; +pub mod jwe; diff --git a/kbs/src/policy_engine/mod.rs b/kbs/src/policy_engine/mod.rs index 995dea2bd..6ff3838a3 100644 --- a/kbs/src/policy_engine/mod.rs +++ b/kbs/src/policy_engine/mod.rs @@ -40,17 +40,17 @@ pub(crate) trait PolicyEngineInterface: Send + Sync { } /// Policy engine configuration. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq)] pub struct PolicyEngineConfig { /// Path to a file containing a policy for evaluating whether the TCB status has access to /// specific resources. - pub policy_path: Option, + pub policy_path: PathBuf, } impl Default for PolicyEngineConfig { fn default() -> Self { Self { - policy_path: Some(PathBuf::from(DEFAULT_POLICY_PATH)), + policy_path: PathBuf::from(DEFAULT_POLICY_PATH), } } } diff --git a/kbs/src/policy_engine/opa/mod.rs b/kbs/src/policy_engine/opa/mod.rs index 372ec54e0..08ac27508 100644 --- a/kbs/src/policy_engine/opa/mod.rs +++ b/kbs/src/policy_engine/opa/mod.rs @@ -116,7 +116,7 @@ mod tests { let policy = std::fs::read(PathBuf::from(path.to_string())).unwrap(); let policy = URL_SAFE_NO_PAD.encode(policy); - opa.set_policy(policy).await + opa.set_policy(&policy).await } #[tokio::test] @@ -130,7 +130,7 @@ mod tests { .unwrap(); // decode error - let malformed_policy = "123".to_string(); + let malformed_policy = "123"; let res = opa.set_policy(malformed_policy).await; assert!(matches!( res.err().unwrap(), @@ -192,10 +192,8 @@ mod tests { set_policy_from_file(&mut opa, policy_path).await.unwrap(); - let resource_path = resource_path.to_string(); - let res = opa - .evaluate(resource_path.clone(), dummy_input(input_name, input_svn)) + .evaluate(resource_path, &dummy_input(input_name, input_svn)) .await; if let Ok(actual) = res { diff --git a/kbs/test_data/configs/coco-as-builtin-1.toml b/kbs/test_data/configs/coco-as-builtin-1.toml new file mode 100644 index 000000000..7fd6da32c --- /dev/null +++ b/kbs/test_data/configs/coco-as-builtin-1.toml @@ -0,0 +1,8 @@ +[attestation_service] +type = "coco_as_builtin" +work_dir = "/opt/coco/attestation-service" +policy_engine = "opa" +attestation_token_broker = "Simple" + + [attestation_service.rvps_config] + remote_addr = "http://127.0.0.1:50003" diff --git a/kbs/test_data/configs/coco-as-builtin-2.toml b/kbs/test_data/configs/coco-as-builtin-2.toml new file mode 100644 index 000000000..b5e1006a5 --- /dev/null +++ b/kbs/test_data/configs/coco-as-builtin-2.toml @@ -0,0 +1,23 @@ +[http_server] +sockets = ["0.0.0.0:8080"] +# Ideally we should use some solution like cert-manager to issue let's encrypt based certificate: +# https://cert-manager.io/docs/configuration/acme/ +insecure_http = true + +[attestation_token] + +[attestation_service] +type = "coco_as_builtin" +work_dir = "/opt/confidential-containers/attestation-service" +policy_engine = "opa" +attestation_token_broker = "Simple" + + [attestation_service.attestation_token_config] + duration_min = 5 + + [attestation_service.rvps_config] + store_type = "LocalFs" + remote_addr = "" + +[admin] +auth_public_key = "/kbs/kbs.pem" \ No newline at end of file diff --git a/kbs/test_data/configs/coco-as-builtin-3.toml b/kbs/test_data/configs/coco-as-builtin-3.toml new file mode 100644 index 000000000..20d8df7b8 --- /dev/null +++ b/kbs/test_data/configs/coco-as-builtin-3.toml @@ -0,0 +1,28 @@ +[http_server] +insecure_http = true + +[attestation_token_config] +type = "CoCo" + +[repository] +type = "LocalFs" +dir_path = "/opt/confidential-containers/kbs/repository" + +[attestation_service] +type = "coco_as_builtin" +work_dir = "/opt/confidential-containers/attestation-service" +policy_engine = "opa" +attestation_token_broker = "Simple" + + [attestation_service.attestation_token_config] + duration_min = 5 + + [attestation_service.rvps_config] + store_type = "LocalFs" + remote_addr = "" + +[policy_engine] +policy_path = "/opa/confidential-containers/kbs/policy.rego" + +[admin] +insecure_api = true \ No newline at end of file diff --git a/kbs/test_data/configs/coco-as-grpc-1.toml b/kbs/test_data/configs/coco-as-grpc-1.toml new file mode 100644 index 000000000..d9e3f8d9c --- /dev/null +++ b/kbs/test_data/configs/coco-as-grpc-1.toml @@ -0,0 +1,29 @@ +[repository] +type = "LocalFs" +dir_path = "/tmp/kbs-resource" + +[attestation_token] +trusted_certs_paths = ["/etc/ca", "/etc/ca2"] + +[attestation_service] +type = "coco_as_grpc" +as_addr = "http://127.0.0.1:50001" +pool_size = 100 +timeout = 600 + +[http_server] +sockets = ["0.0.0.0:8080"] +private_key = "/etc/kbs-private.key" +certificate = "/etc/kbs-cert.pem" +insecure_http = false + +[admin] +auth_public_key = "/etc/kbs-admin.pub" +insecure_api = false + +[policy_engine] +policy_path = "/etc/kbs-policy.rego" + +[[client_plugins]] +name = "sample" +item = "value1" \ No newline at end of file diff --git a/kbs/test_data/configs/coco-as-grpc-2.toml b/kbs/test_data/configs/coco-as-grpc-2.toml new file mode 100644 index 000000000..a0aedc0d5 --- /dev/null +++ b/kbs/test_data/configs/coco-as-grpc-2.toml @@ -0,0 +1,12 @@ +[http_server] +sockets = ["0.0.0.0:8080"] +insecure_http = true + +[attestation_token] + +[attestation_service] +type = "coco_as_grpc" +as_addr = "http://as:50004" + +[admin] +auth_public_key = "/opt/confidential-containers/kbs/user-keys/public.pub" \ No newline at end of file diff --git a/kbs/test_data/configs/coco-as-grpc-3.toml b/kbs/test_data/configs/coco-as-grpc-3.toml new file mode 100644 index 000000000..c6d8e61e3 --- /dev/null +++ b/kbs/test_data/configs/coco-as-grpc-3.toml @@ -0,0 +1,12 @@ +[http_server] +insecure_http = true + +[attestation_token] + +[attestation_service] +type = "coco_as_grpc" +as_addr = "http://127.0.0.1:50004" +pool_size = 100 + +[admin] +insecure_api = true \ No newline at end of file diff --git a/kbs/test_data/configs/intel-ta-1.toml b/kbs/test_data/configs/intel-ta-1.toml new file mode 100644 index 000000000..281b1ea40 --- /dev/null +++ b/kbs/test_data/configs/intel-ta-1.toml @@ -0,0 +1,30 @@ +[repository] +type = "LocalFs" +dir_path = "/tmp/kbs-resource" + +[attestation_token] +trusted_jwk_sets = ["/etc/ca", "/etc/ca2"] + +[attestation_service] +type = "intel_ta" +base_url = "example.io" +api_key = "this-is-a-key" +certs_file = "file:///etc/ita-cert.pem" +allow_unmatched_policy = true + +[http_server] +sockets = ["0.0.0.0:8080"] +private_key = "/etc/kbs-private.key" +certificate = "/etc/kbs-cert.pem" +insecure_http = false + +[admin] +auth_public_key = "/etc/kbs-admin.pub" +insecure_api = false + +[policy_engine] +policy_path = "/etc/kbs-policy.rego" + +[[client_plugins]] +name = "sample" +item = "value1" \ No newline at end of file diff --git a/kbs/test_data/configs/intel-ta-2.toml b/kbs/test_data/configs/intel-ta-2.toml new file mode 100644 index 000000000..3c7714430 --- /dev/null +++ b/kbs/test_data/configs/intel-ta-2.toml @@ -0,0 +1,17 @@ +[http_server] +sockets = ["0.0.0.0:8080"] +# Ideally we should use some solution like cert-manager to issue let's encrypt based certificate: +# https://cert-manager.io/docs/configuration/acme/ +insecure_http = true + +[attestation_token] +trusted_jwk_sets = ["https://portal.trustauthority.intel.com"] + +[attestation_service] +type = "intel_ta" +base_url = "https://api.trustauthority.intel.com" +api_key = "tBfd5kKX2x9ahbodKV1..." +certs_file = "https://portal.trustauthority.intel.com" + +[admin] +auth_public_key = "/kbs/kbs.pem" \ No newline at end of file diff --git a/kbs/test_data/configs/intel-ta-3.toml b/kbs/test_data/configs/intel-ta-3.toml new file mode 100644 index 000000000..90b158059 --- /dev/null +++ b/kbs/test_data/configs/intel-ta-3.toml @@ -0,0 +1,14 @@ +[http_server] +insecure_http = true + +[admin] +insecure_api = true + +[attestation_token] +trusted_jwk_sets = ["https://portal.trustauthority.intel.com"] + +[attestation_service] +type = "intel_ta" +base_url = "https://api.trustauthority.intel.com" +api_key = "tBfd5kKX2x9ahbodKV1..." +certs_file = "https://portal.trustauthority.intel.com" From 71573e2ce97c3a60444e498ed2dcfc268ba7a2f0 Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Fri, 27 Sep 2024 15:17:52 +0800 Subject: [PATCH 06/10] KBS: fix CI, docs and exampled configurations This patch fixes example configurations of KBS inside this codebase. Also, it fixes the CI test and the docs. Signed-off-by: Xynnn007 --- .github/workflows/kbs-docker-build.yml | 1 - .github/workflows/kbs-rust.yml | 4 +- deps/verifier/src/se/README.md | 2 +- kbs/Makefile | 5 +- kbs/README.md | 6 +- kbs/config/docker-compose/kbs-config.toml | 15 +- kbs/config/kbs-config-grpc.toml | 12 +- .../kbs-config-intel-trust-authority.toml | 10 +- kbs/config/kbs-config.toml | 26 +- kbs/config/kubernetes/base/kbs-config.toml | 20 +- kbs/config/kubernetes/ita/kbs-config.toml | 10 +- kbs/docker/Dockerfile | 2 +- kbs/docker/coco-as-grpc/Dockerfile | 2 +- kbs/docker/intel-trust-authority/Dockerfile | 2 +- kbs/docker/rhel-ubi/Dockerfile | 2 +- kbs/docs/config.md | 299 +++++++++++------- kbs/docs/self-signed-https.md | 4 +- kbs/quickstart.md | 4 +- kbs/test/Makefile | 4 +- kbs/test/config/kbs.toml | 35 +- kbs/test/config/resource-kbs.toml | 12 +- kbs/test_data/configs/coco-as-builtin-3.toml | 3 +- 22 files changed, 290 insertions(+), 190 deletions(-) diff --git a/.github/workflows/kbs-docker-build.yml b/.github/workflows/kbs-docker-build.yml index 96faee36b..ce7e61bcf 100644 --- a/.github/workflows/kbs-docker-build.yml +++ b/.github/workflows/kbs-docker-build.yml @@ -16,7 +16,6 @@ jobs: - name: Build KBS Container Image run: | DOCKER_BUILDKIT=1 docker build -t kbs:coco-as . -f kbs/docker/Dockerfile; \ - DOCKER_BUILDKIT=1 docker build -t kbs:coco-as-openssl --build-arg KBS_FEATURES=coco-as-builtin,openssl,resource,opa . -f kbs/docker/Dockerfile; \ DOCKER_BUILDKIT=1 docker build -t kbs:coco-as-grpc . -f kbs/docker/coco-as-grpc/Dockerfile; \ DOCKER_BUILDKIT=1 docker build -t kbs:coco-as-rhel-ubi . -f kbs/docker/rhel-ubi/Dockerfile; \ DOCKER_BUILDKIT=1 docker build -t kbs:coco-as-ita . -f kbs/docker/intel-trust-authority/Dockerfile diff --git a/.github/workflows/kbs-rust.yml b/.github/workflows/kbs-rust.yml index ab6fefb0c..eacd5a6aa 100644 --- a/.github/workflows/kbs-rust.yml +++ b/.github/workflows/kbs-rust.yml @@ -56,11 +56,11 @@ jobs: working-directory: kbs run: make - - name: KBS Build [Built-in CoCo AS, OpenSSL] + - name: KBS Build [Built-in CoCo AS] working-directory: kbs run: make - - name: KBS Build [gRPC CoCo AS, RustTLS] + - name: KBS Build [gRPC CoCo AS] working-directory: kbs run: make COCO_AS_INTEGRATE_TYPE=grpc diff --git a/deps/verifier/src/se/README.md b/deps/verifier/src/se/README.md index d2cfa5a4b..569d4c92f 100644 --- a/deps/verifier/src/se/README.md +++ b/deps/verifier/src/se/README.md @@ -100,7 +100,7 @@ auth_public_key = "/kbs/kbs.pem" # https://cert-manager.io/docs/configuration/acme/ insecure_http = true -[attestation_token_config] +[attestation_token] insecure_key = true [as_config] diff --git a/kbs/Makefile b/kbs/Makefile index 90c6267d3..04652a016 100644 --- a/kbs/Makefile +++ b/kbs/Makefile @@ -1,5 +1,4 @@ AS_TYPE ?= coco-as -POLICY_ENGINE ?= ALIYUN ?= false ARCH := $(shell uname -m) @@ -38,7 +37,7 @@ build: background-check-kbs .PHONY: background-check-kbs background-check-kbs: - cargo build -p kbs --locked --release --no-default-features --features $(AS_FEATURE),resource,$(POLICY_ENGINE),$(FEATURES) + cargo build -p kbs --locked --release --no-default-features --features $(AS_FEATURE),resource,$(FEATURES) .PHONY: passport-issuer-kbs passport-issuer-kbs: @@ -47,7 +46,7 @@ passport-issuer-kbs: .PHONY: passport-resource-kbs passport-resource-kbs: - cargo build -p kbs --locked --release --no-default-features --features resource,$(POLICY_ENGINE),$(FEATURES) + cargo build -p kbs --locked --release --no-default-features --features resource,$(FEATURES) mv ../target/release/kbs ../target/release/resource-kbs .PHONY: cli diff --git a/kbs/README.md b/kbs/README.md index 6b6e69c6e..f09a92626 100644 --- a/kbs/README.md +++ b/kbs/README.md @@ -90,18 +90,16 @@ The Makefile supports a number of other configuration parameters. For example, ```shell -make background-check-kbs [POLICY_ENGINE=?] [AS_TYPES=?] [COCO_AS_INTEGRATION_TYPE=?] [ALIYUN=?] +make background-check-kbs [AS_TYPES=?] [COCO_AS_INTEGRATION_TYPE=?] [ALIYUN=?] ``` The parameters -- `POLICY_ENGINE`: The KBS has a policy engine to facilitate access control. This should not be confused with the policy engine in the AS, which determines whether or not TEE evidence is valid. `POLICY_ENGINE` determines which type of policy engine the KBS will use. Today only `opa` is supported. The KBS can also be built without a policy engine -if it is not required. - `AS_TYPES`: The KBS supports multiple backend attestation services. `AS_TYPES` selects which verifier to use. The options are `coco-as` and `intel-trust-authority-as`. - `COCO_AS_INTEGRATION_TYPE`: The KBS can connect to the CoCo AS in multiple ways. `COCO_AS_INTEGRATION_TYPE` can be set either to `grpc` or `builtin`. With `grpc` the KBS will make a remote connection to the AS. If you are manually building and configuring the components, you'll need to set them up so that this connection can be established. Similar to passport mode, the remote AS can be useful if secret provisioning and attestation verification are not in the same scope. With `builtin` the KBA uses the AS as a crate. This is recommended if you want to avoid the complexity of a remote connection. - `ALIYUN`: The kbs support aliyun KMS as secret storage backend. `true` to enable building this feature. By default it is `false`. ## HTTPS Support -The KBS can use HTTPS. This requires a crypto backend. +The KBS can use HTTPS. This is facilitated by openssl crypto backend. If you want a self-signed cert for test cases, please refer to [the document](docs/self-signed-https.md). diff --git a/kbs/config/docker-compose/kbs-config.toml b/kbs/config/docker-compose/kbs-config.toml index 461d2aa54..9bb770fc9 100644 --- a/kbs/config/docker-compose/kbs-config.toml +++ b/kbs/config/docker-compose/kbs-config.toml @@ -1,9 +1,18 @@ +[http_server] sockets = ["0.0.0.0:8080"] -auth_public_key = "/opt/confidential-containers/kbs/user-keys/public.pub" insecure_http = true -[attestation_token_config] +[attestation_token] insecure_key = true -[grpc_config] +[attestation_service] +type = "coco_as_grpc" as_addr = "http://as:50004" + +[admin] +auth_public_key = "/opt/confidential-containers/kbs/user-keys/public.pub" + +[[plugins]] +name = "resource" +type = "LocalFs" +dir_path = "/opt/confidential-containers/kbs/repository" \ No newline at end of file diff --git a/kbs/config/kbs-config-grpc.toml b/kbs/config/kbs-config-grpc.toml index 4bc596917..45d2c0865 100644 --- a/kbs/config/kbs-config-grpc.toml +++ b/kbs/config/kbs-config-grpc.toml @@ -1,9 +1,13 @@ +[http_server] insecure_http = true -insecure_api = true -[attestation_token_config] +[attestation_token] insecure_key = true -[grpc_config] +[attestation_service] +type = "coco_as_grpc" as_addr = "http://127.0.0.1:50004" -pool_size = 200 \ No newline at end of file +pool_size = 200 + +[admin] +insecure_api = true \ No newline at end of file diff --git a/kbs/config/kbs-config-intel-trust-authority.toml b/kbs/config/kbs-config-intel-trust-authority.toml index 070841da6..90b158059 100644 --- a/kbs/config/kbs-config-intel-trust-authority.toml +++ b/kbs/config/kbs-config-intel-trust-authority.toml @@ -1,10 +1,14 @@ +[http_server] insecure_http = true + +[admin] insecure_api = true -[attestation_token_config] -trusted_certs_paths = ["https://portal.trustauthority.intel.com"] +[attestation_token] +trusted_jwk_sets = ["https://portal.trustauthority.intel.com"] -[intel_trust_authority_config] +[attestation_service] +type = "intel_ta" base_url = "https://api.trustauthority.intel.com" api_key = "tBfd5kKX2x9ahbodKV1..." certs_file = "https://portal.trustauthority.intel.com" diff --git a/kbs/config/kbs-config.toml b/kbs/config/kbs-config.toml index 7b99f0359..b51069608 100644 --- a/kbs/config/kbs-config.toml +++ b/kbs/config/kbs-config.toml @@ -1,24 +1,28 @@ +[http_server] insecure_http = true -insecure_api = true -[attestation_token_config] -insecure_key = true +[attestation_token] +insecure_api = true -[repository_config] +[repository] type = "LocalFs" dir_path = "/opt/confidential-containers/kbs/repository" -[as_config] +[attestation_service] +type = "coco_as_builtin" work_dir = "/opt/confidential-containers/attestation-service" policy_engine = "opa" attestation_token_broker = "Simple" -[as_config.attestation_token_config] -duration_min = 5 + [attestation_service.attestation_token_config] + duration_min = 5 -[as_config.rvps_config] -store_type = "LocalFs" -remote_addr = "" + [attestation_service.rvps_config] + store_type = "LocalFs" + remote_addr = "" -[policy_engine_config] +[policy_engine] policy_path = "/opa/confidential-containers/kbs/policy.rego" + +[admin] +insecure_api = true \ No newline at end of file diff --git a/kbs/config/kubernetes/base/kbs-config.toml b/kbs/config/kubernetes/base/kbs-config.toml index 67d01a6ff..5256b8c8d 100644 --- a/kbs/config/kubernetes/base/kbs-config.toml +++ b/kbs/config/kubernetes/base/kbs-config.toml @@ -1,20 +1,24 @@ +[http_server] sockets = ["0.0.0.0:8080"] -auth_public_key = "/kbs/kbs.pem" # Ideally we should use some solution like cert-manager to issue let's encrypt based certificate: # https://cert-manager.io/docs/configuration/acme/ insecure_http = true -[attestation_token_config] +[attestation_token] insecure_key = true -[as_config] +[attestation_service] +type = "coco_as_builtin" work_dir = "/opt/confidential-containers/attestation-service" policy_engine = "opa" attestation_token_broker = "Simple" -[as_config.attestation_token_config] -duration_min = 5 + [attestation_service.attestation_token_config] + duration_min = 5 -[as_config.rvps_config] -store_type = "LocalFs" -remote_addr = "" + [attestation_service.rvps_config] + store_type = "LocalFs" + remote_addr = "" + +[admin] +auth_public_key = "/kbs/kbs.pem" \ No newline at end of file diff --git a/kbs/config/kubernetes/ita/kbs-config.toml b/kbs/config/kubernetes/ita/kbs-config.toml index 044864e78..ef942819c 100644 --- a/kbs/config/kubernetes/ita/kbs-config.toml +++ b/kbs/config/kubernetes/ita/kbs-config.toml @@ -1,13 +1,17 @@ +[http_server] sockets = ["0.0.0.0:8080"] -auth_public_key = "/kbs/kbs.pem" # Ideally we should use some solution like cert-manager to issue let's encrypt based certificate: # https://cert-manager.io/docs/configuration/acme/ insecure_http = true -[attestation_token_config] +[attestation_token] trusted_certs_paths = ["https://portal.trustauthority.intel.com"] -[intel_trust_authority_config] +[attestation_service] +type = "intel_ta" base_url = "https://api.trustauthority.intel.com" api_key = "tBfd5kKX2x9ahbodKV1..." certs_file = "https://portal.trustauthority.intel.com" + +[admin] +auth_public_key = "/kbs/kbs.pem" \ No newline at end of file diff --git a/kbs/docker/Dockerfile b/kbs/docker/Dockerfile index f6bd8294a..121a01e83 100644 --- a/kbs/docker/Dockerfile +++ b/kbs/docker/Dockerfile @@ -36,7 +36,7 @@ RUN if [ "${ARCH}" = "x86_64" ]; then curl -fsSL https://download.01.org/intel-s WORKDIR /usr/src/kbs COPY . . -RUN cd kbs && make AS_FEATURE=coco-as-builtin POLICY_ENGINE=opa ALIYUN=${ALIYUN} && \ +RUN cd kbs && make AS_FEATURE=coco-as-builtin ALIYUN=${ALIYUN} && \ make install-kbs FROM ubuntu:22.04 diff --git a/kbs/docker/coco-as-grpc/Dockerfile b/kbs/docker/coco-as-grpc/Dockerfile index 67f099e6a..a0bb8440b 100644 --- a/kbs/docker/coco-as-grpc/Dockerfile +++ b/kbs/docker/coco-as-grpc/Dockerfile @@ -8,7 +8,7 @@ COPY . . RUN apt-get update && apt install -y protobuf-compiler git # Build and Install KBS -RUN cd kbs && make AS_FEATURE=coco-as-grpc POLICY_ENGINE=opa ALIYUN=${ALIYUN} && \ +RUN cd kbs && make AS_FEATURE=coco-as-grpc ALIYUN=${ALIYUN} && \ make install-kbs FROM ubuntu:22.04 diff --git a/kbs/docker/intel-trust-authority/Dockerfile b/kbs/docker/intel-trust-authority/Dockerfile index 0638b9cf8..690fb264c 100644 --- a/kbs/docker/intel-trust-authority/Dockerfile +++ b/kbs/docker/intel-trust-authority/Dockerfile @@ -7,7 +7,7 @@ COPY . . RUN apt-get update && apt install -y git # Build and Install KBS -RUN cd kbs && make AS_FEATURE=intel-trust-authority-as POLICY_ENGINE=opa ALIYUN=${ALIYUN} && \ +RUN cd kbs && make AS_FEATURE=intel-trust-authority-as ALIYUN=${ALIYUN} && \ make install-kbs FROM ubuntu:22.04 diff --git a/kbs/docker/rhel-ubi/Dockerfile b/kbs/docker/rhel-ubi/Dockerfile index a49ee0aae..61f6269ed 100644 --- a/kbs/docker/rhel-ubi/Dockerfile +++ b/kbs/docker/rhel-ubi/Dockerfile @@ -15,7 +15,7 @@ dnf -y install --nogpgcheck --repofrompath "sgx,file:///root/sgx_rpm_local_repo" # Build. WORKDIR /usr/src/kbs COPY . . -ARG KBS_FEATURES=coco-as-builtin,resource,opa +ARG KBS_FEATURES=coco-as-builtin RUN \ cargo install --locked --root /usr/local/ --path kbs --bin kbs --no-default-features --features ${KBS_FEATURES} && \ # Collect linked files necessary for the binary to run. diff --git a/kbs/docs/config.md b/kbs/docs/config.md index 1d2498bf1..63013534b 100644 --- a/kbs/docs/config.md +++ b/kbs/docs/config.md @@ -16,33 +16,31 @@ environment variable. The following sections list the KBS properties which can be set through the configuration file. -### Global Properties +### HTTP Server Configuration -The following properties can be set globally, i.e. not under any configuration -section: +The following properties can be set under the `[http_server]` section. | Property | Type | Description | Required | Default | |--------------------------|--------------|------------------------------------------------------------------------------------------------------------|----------|----------------------| | `sockets` | String array | One or more sockets to listen on. | No | `["127.0.0.1:8080"]` | -| `insecure_api` | Boolean | Enable KBS insecure APIs such as Resource Registration without JWK verification. | No | `false` | | `insecure_http` | Boolean | Don't use TLS for the KBS HTTP endpoint. | No | `false` | -| `timeout` | Integer | HTTP session timeout in minutes. | No | `5` | -| `private_key` | String | Path to a private key file to be used for HTTPS. | No | - | -| `certificate` | String | Path to a certificate file to be used for HTTPS. | No | - | -| `auth_public_key` | String | Path to a public key file to be used for authenticating the resource registration endpoint token (JWT). | No | - | +| `private_key` | String | Path to a private key file to be used for HTTPS. | No | None | +| `certificate` | String | Path to a certificate file to be used for HTTPS. | No | None | ### Attestation Token Configuration -The following properties can be set under the `attestation_token_config` section. +Attestation Token configuration controls attestation token verifications. This +is important when a resource retrievement is handled by KBS. Usually an attestation +token will be together with the request, and KBS will first verify the token. ->This section is available only when the `resource` feature is enabled. +The following properties can be set under the `[attestation_token]` section. -| Property | Type | Description | Required | Default | -|----------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------| -| `trusted_jwk_sets` | String Array | Valid Url (`file://` or `https://`) pointing to trusted JWKSets (local or OpenID) for Attestation Tokens trustworthy verification | No | - | -| `trusted_certs_paths` | String Array | Trusted Certificates file (PEM format) for Attestation Tokens trustworthy verification | No | - | -| `extra_teekey_paths` | String Array | User defined paths to the tee public key in the JWT body | No | - | -| `insecure_key` | Boolean | Whether to check the trustworthy of the JWK inside JWT. See comments. | No | `false` | +| Property | Type | Description | Default | +|----------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| +| `trusted_jwk_sets` | String Array | Valid Url (`file://` or `https://`) pointing to trusted JWKSets (local or OpenID) for Attestation Tokens trustworthy verification | Empty | +| `trusted_certs_paths` | String Array | Trusted Certificates file (PEM format) for Attestation Tokens trustworthy verification | Empty | +| `extra_teekey_paths` | String Array | User defined paths to the tee public key in the JWT body | Empty | +| `insecure_key` | Boolean | Whether to check the trustworthy of the JWK inside JWT. See comments. | `false` | Each JWT contains a TEE Public Key. Users can use the `extra_teekey_paths` field to additionally specify the path of this Key in the JWT. Example of `extra_teekey_paths` is `/attester_runtime_data/tee-pubkey` which refers to the key @@ -60,117 +58,114 @@ For Attestation Services like Intel TA, there will only be a `kid` field inside The `kid` field is used to look up the trusted jwk configured by KBS via `trusted_jwk_sets` to verify the integrity and trustworthy of the JWT. -### Repository Configuration +### Attestation Configuration -The following properties can be set under the `repository_config` section. +Attestation configuration defines the attestation service that KBS' RCAR protocol +will leverage. -This section is **optional**. When omitted, a default configuration is used. - -Repository configuration is **specific to a repository type**. See the following sections for -type-specific properties. +The following properties can be set under the `[attestation_service]` section. ->This section is available only when the `resource` feature is enabled. Only one repository is available at a time. +Concrete attestation service can be set via `type` field. Supported attestation +services are +- `coco_as_builtin`: CoCo AS that built inside KBS binary +- `coco_as_grpc`: CoCo AS service running remotely +- `intel_ta`: Intel® Trust Authority -| Property | Type | Description | Required | Default | -|----------|--------|-----------------------------------------------------------------|----------|-----------| -| `type` | String | The resource repository type. Valid values: `LocalFs`, `Aliyun` | Yes | `LocalFs` | +Due to different `type` field, properties are different. -**`LocalFs` Properties** +#### Built-In CoCo AS -| Property | Type | Description | Required | Default | -|------------|--------|---------------------------------|----------|-----------------------------------------------------| -| `dir_path` | String | Path to a repository directory. | No | `/opt/confidential-containers/kbs/repository` | - -**`Aliyun` Properties** +When `type` is set to `coco_as_builtin`, the following properties can be set. -| Property | Type | Description | Required | Example | -|-------------------|--------|-----------------------------------|----------|-----------------------------------------------------| -| `client_key` | String | The KMS instance's AAP client key | Yes | `{"KeyId": "KA..", "PrivateKeyData": "MIIJqwI..."}` | -| `kms_instance_id` | String | The KMS instance id | Yes | `kst-shh668f7...` | -| `password` | String | AAP client key password | Yes | `8f9989c18d27...` | -| `cert_pem` | String | CA cert for the KMS instance | Yes | `-----BEGIN CERTIFICATE----- ...` | - -### Native Attestation - -The following properties can be set under the `as_config` section. - -This section is **optional**. When omitted, a default configuration is used. - ->This section is available only when one or more of the following features are enabled: +>Built-In CoCo AS is available only when one or more of the following features are enabled: >`coco-as-builtin`, `coco-as-builtin-no-verifier` -| Property | Type | Description | Required | Default | -|----------------------------|-----------------------------|-----------------------------------------------------|----------|---------| -| `work_dir` | String | The location for Attestation Service to store data. | Yes | - | -| `policy_engine` | String | Policy engine type. Valid values: `opa` | Yes | - | -| `rvps_config` | [RVPSConfiguration][2] | RVPS configuration | Yes | - | -| `attestation_token_broker` | String | Type of the attestation result token broker. | Yes | - | -| `attestation_token_config` | [AttestationTokenConfig][1] | Attestation result token configuration. | Yes | - | +| Property | Type | Description | Default | +|----------------------------|-----------------------------|-----------------------------------------------------|----------| +| `timeout` | Integer | The maximum time (in minutes) between RCAR handshake's `auth` and `attest` requests | 5 | +| `work_dir` | String | The location for Attestation Service to store data. | First try from env `AS_WORK_DIR`. If no this env, then use `/opt/confidential-containers/attestation-service` | +| `policy_engine` | String | Policy engine type. Valid values: `opa` | `opa` | +| `rvps_config` | [RVPSConfiguration][2] | RVPS configuration | See [RVPSConfiguration][2] | +| `attestation_token_broker` | String | Type of the attestation result token broker. | `Simple` | +| `attestation_token_config` | [AttestationTokenConfig][1] | Attestation result token configuration. | See [AttestationTokenConfig][1] | [1]: #attestationtokenconfig [2]: #rvps-configuration -#### AttestationTokenConfig -| Property | Type | Description | Required | Default | -|----------------|-------------------------|------------------------------------------------------|----------|---------| -| `duration_min` | Integer | Duration of the attestation result token in minutes. | Yes | - | -| `issuer_name` | String | Issure name of the attestation result token. | No | - | -| `signer` | [TokenSignerConfig][1] | Signing material of the attestation result token. | No | - | +##### AttestationTokenConfig + +| Property | Type | Description | Default | +|----------------|-------------------------|------------------------------------------------------|----------| +| `duration_min` | Integer | Duration of the attestation result token in minutes. | 5 | +| `issuer_name` | String | Issure name of the attestation result token. | `CoCo-Attestation-Service` | +| `signer` | [TokenSignerConfig][1] | Signing material of the attestation result token. | None | [1]: #tokensignerconfig -#### TokenSignerConfig +##### TokenSignerConfig -This section is **optional**. When omitted, a new RSA key pair is generated and used. +This section is **optional**. When omitted, an ephemeral RSA key pair is generated and used. -| Property | Type | Description | Required | Default | -|----------------|---------|----------------------------------------------------------|----------|---------| -| `key_path` | String | RSA Key Pair file (PEM format) path. | Yes | - | -| `cert_url` | String | RSA Public Key certificate chain (PEM format) URL. | No | - | -| `cert_path` | String | RSA Public Key certificate chain (PEM format) file path. | No | - | +| Property | Type | Description | Required | +|----------------|---------|----------------------------------------------------------|----------| +| `key_path` | String | RSA Key Pair file (PEM format) path. | Yes | +| `cert_url` | String | RSA Public Key certificate chain (PEM format) URL. | No | +| `cert_path` | String | RSA Public Key certificate chain (PEM format) file path. | No | -#### RVPS Configuration +##### RVPS Configuration -| Property | Type | Description | Required | Default | -|----------------|-------------------------|------------------------------------------------------|----------|---------| -| `remote_addr` | String | Remote RVPS' address. If this is specified, will use a remote RVPS. Or a local RVPS will be configured with `store_type` and `store_config`| Conditional | - | -| `store_type` | String | Used if `remote_addr` is not set. The underlying storage type of RVPS. | Conditional | - | -| `store_config` | JSON Map | Used if `remote_addr` is not set. The optional configurations to the underlying storage. | Conditional | - | +| Property | Type | Description | Default | +|----------------|-------------------------|------------------------------------------------------|---------| +| `remote_addr` | String | Remote RVPS' address. If this is specified, will use a remote RVPS. Or a local RVPS will be configured with `store_type` and `store_config`| Empty | +| `store_type` | String | Used if `remote_addr` is not set. The underlying storage type of RVPS. | `LocalFs` | +| `store_config` | JSON Map | Used if `remote_addr` is not set. The optional configurations to the underlying storage. | Empty | Different `store_type` will have different `store_config` items. See the details of `store_config` in [concrete implementations of storages](../../rvps/src/store/). -### gRPC Attestation +#### gRPC CoCo AS -The following properties can be set under the `grpc_config` section. +When `type` is set to `coco_as_grpc`, KBS will try to connect a remote CoCo AS for +attestation. The following properties can be set. -This section is **optional**. When omitted, a default configuration is used. +>gRPC CoCo AS is available only when `coco-as-grpc` feature is enabled. ->This section is available only when the `coco-as-grpc` feature is enabled. +| Property | Type | Description | Default | +|----------------------------|-----------------------------|-----------------------------------------------------|----------| +| `timeout` | Integer | The maximum time (in minutes) between RCAR handshake's `auth` and `attest` requests | 5 | +| `as_addr` | String | The URL of the remote CoCoAS | `http://127.0.0.1:50004` | +| `pool_size` | Integer | The connections between KBS and CoCoAS are maintained in a conenction pool. This property determines the max size of the pool | `100` | -| Property | Type | Description | Required | Default | -|-----------|--------|------------------------------|----------|--------------------------| -| `as_addr` | String | Attestation service address. | No | `http://127.0.0.1:50004` | +#### Intel® TA -### Intel Trust Authority (formerly known as Amber) +When `type` is set to `intel_ta`, KBS will try to connect a remote Intel TA service for +attestation. The following properties can be set. -The following properties can be set under the `intel_trust_authority_config` section. +>gRPC CoCo AS is available only when `coco-as-grpc` feature is enabled. ->This section is available only when the `intel-trust-authority-as` feature is enabled. - -| Property | Type | Description | Required | Default | -|--------------------------|---------|------------------------------------------------------------------------------------------|-------------------------|---------| -| `base_url` | String | Intel Trust Authority API URL. | Yes | - | -| `api_key` | String | Intel Trust Authority API key. | Yes | - | -| `certs_file` | String | URL to an Intel Trust Authority portal or path to JWKS file used for token verification. | Yes | - | -| `allow_unmatched_policy` | Boolean | Determines whether to ignore the `policy_ids_unmatched` token claim. | No | false | +| Property | Type | Description | Required | Default | +|--------------------------|---------|------------------------------------------------------------------------------------------|----------|---------| +| `timeout` | Integer | The maximum time (in minutes) between RCAR handshake's `auth` and `attest` requests | No | 5 | +| `base_url` | String | Intel Trust Authority API URL. | Yes | - | +| `api_key` | String | Intel Trust Authority API key. | Yes | - | +| `certs_file` | String | URL to an Intel Trust Authority portal or path to JWKS file used for token verification. | Yes | - | +| `allow_unmatched_policy` | Boolean | Determines whether to ignore the `policy_ids_unmatched` token claim. | No | false | Detailed [documentation](https://docs.trustauthority.intel.com). +### Admin API Configuration + +The following properties can be set under the `[admin]` section. + +| Property | Type | Description | Required | Default | +|--------------------------|--------------|------------------------------------------------------------------------------------------------------------|----------|----------------------| +| `auth_public_key` | String | Path to the public key used to authenticate the admin APIs | No | None | +| `insecure_api` | Boolean | Whether KBS will not verify the public key when called admin APIs | No | `false` | + ### Policy Engine Configuration -The following properties can be set under the `policy_engine_config` section. +The following properties can be set under the `[policy_engine]` section. This section is **optional**. When omitted, a default configuration is used. @@ -178,72 +173,144 @@ This section is **optional**. When omitted, a default configuration is used. |--------------------------|---------|------------------------------------------------------------------------------------------------------------|-------------------------|------------------------------------------------| | `policy_path` | String | Path to a file containing a policy for evaluating whether the TCB status has access to specific resources. | No | `/opa/confidential-containers/kbs/policy.rego` | +### Plugins Configuration + +KBS supports different kinds of plugins, and they can be enabled via add corresponding configs. + +Multiple `[[plugins]]` sections are allowed at the same time for different plugins. +Concrete attestation service can be set via `name` field. + +#### Resource Configuration + +The `name` field is `resource` to enable this plugin. + +Resource plugin allows user with proper attestation token to access storage that KBS keeps. +This is also called "Repository" in old versions. The properties to be configured are listed. + +| Property | Type | Description | Required | Default | +|----------|--------|-----------------------------------------------------------------|----------|-----------| +| `type` | String | The resource repository type. Valid values: `LocalFs`, `Aliyun` | Yes | `LocalFs` | + +**`LocalFs` Properties** + +| Property | Type | Description | Required | Default | +|------------|--------|---------------------------------|----------|-----------------------------------------------------| +| `dir_path` | String | Path to a repository directory. | No | `/opt/confidential-containers/kbs/repository` | + +**`Aliyun` Properties** + +| Property | Type | Description | Required | Example | +|-------------------|--------|-----------------------------------|----------|-----------------------------------------------------| +| `client_key` | String | The KMS instance's AAP client key | Yes | `{"KeyId": "KA..", "PrivateKeyData": "MIIJqwI..."}` | +| `kms_instance_id` | String | The KMS instance id | Yes | `kst-shh668f7...` | +| `password` | String | AAP client key password | Yes | `8f9989c18d27...` | +| `cert_pem` | String | CA cert for the KMS instance | Yes | `-----BEGIN CERTIFICATE----- ...` | + ## Configuration Examples -Running with a built-in native attestation service: +Using a built-in CoCo AS: ```toml +[http_server] +sockets = ["0.0.0.0:8080"] insecure_http = true + +[admin] insecure_api = true -[repository_config] -type = "LocalFs" -dir_path = "/opt/confidential-containers/kbs/repository" +[attestation_token] -[as_config] +[attestation_service] +type = "coco_as_builtin" work_dir = "/opt/confidential-containers/attestation-service" policy_engine = "opa" -rvps_store_type = "LocalFs" attestation_token_broker = "Simple" -[as_config.attestation_token_config] -duration_min = 5 + [attestation_service.attestation_token_config] + duration_min = 5 + + [attestation_service.rvps_config] + store_type = "LocalFs" + remote_addr = "" + +[[plugins]] +name = "resource" +type = "LocalFs" +dir_path = "/opt/confidential-containers/kbs/repository" ``` -Running the attestation service remotely: +Using a remote CoCo AS: ```toml +[http_server] insecure_http = true + +[admin] insecure_api = true -[repository_config] +[attestation_service] +type = "coco_as_grpc" +as_addr = "http://127.0.0.1:50004" + +[[plugins]] +name = "resource" type = "LocalFs" dir_path = "/opt/confidential-containers/kbs/repository" - -[grpc_config] -as_addr = "http://127.0.0.1:50004" ``` Running with Intel Trust Authority attestation service: ```toml -insecure_http = true -insecure_api = true +[http_server] +sockets = ["0.0.0.0:8080"] +private_key = "/etc/kbs-private.key" +certificate = "/etc/kbs-cert.pem" +insecure_http = false -[attestation_token_config] +[attestation_token] trusted_jwk_sets = ["https://portal.trustauthority.intel.com"] -[repository_config] -type = "LocalFs" -dir_path = "/opt/confidential-containers/kbs/repository" +[attestation_token] -[intel_trust_authority_config] +[attestation_service] +type = "intel_ta" base_url = "https://api.trustauthority.intel.com" api_key = "tBfd5kKX2x9ahbodKV1..." certs_file = "https://portal.trustauthority.intel.com" allow_unmatched_policy = true + +[admin] +auth_public_key = "/etc/kbs-admin.pub" +insecure_api = false + +[policy_engine] +policy_path = "/etc/kbs-policy.rego" + +[[plugins]] +name = "resource" +type = "LocalFs" +dir_path = "/opt/confidential-containers/kbs/repository" ``` Distributing resources in Passport mode: ```toml +[http_server] +sockets = ["127.0.0.1:50002"] insecure_http = true -insecure_api = true -[repository_config] -type = "LocalFs" -dir_path = "/opt/confidential-containers/kbs/repository" +[admin] +auth_public_key = "./work/kbs.pem" + +[attestation_token] +trusted_certs_paths = ["./work/ca-cert.pem"] +insecure_key = false -[policy_engine_config] -policy_path = "/opt/confidential-containers/kbs/policy.rego" +[policy_engine] +policy_path = "./work/kbs-policy.rego" + +[[plugins]] +name = "resource" +type = "LocalFs" +dir_path = "./work/repository" ``` diff --git a/kbs/docs/self-signed-https.md b/kbs/docs/self-signed-https.md index 4c36f62f8..e278ba51f 100644 --- a/kbs/docs/self-signed-https.md +++ b/kbs/docs/self-signed-https.md @@ -71,7 +71,7 @@ auth_public_key = "/etc/public.pub" insecure_api = true -[attestation_token_config] +[attestation_token] insecure_key = true [repository_config] @@ -84,7 +84,7 @@ policy_engine = "opa" rvps_store_type = "LocalFs" attestation_token_broker = "Simple" -[as_config.attestation_token_config] +[as_config.attestation_token] duration_min = 5 [as_config.rvps_config] diff --git a/kbs/quickstart.md b/kbs/quickstart.md index 5466fff13..8447f3057 100644 --- a/kbs/quickstart.md +++ b/kbs/quickstart.md @@ -48,7 +48,7 @@ and pass it to the KBS server through startup parameters. Build KBS in Background Check mode: ```shell -make background-check-kbs POLICY_ENGINE=opa +make background-check-kbs sudo make install-kbs ``` @@ -112,7 +112,7 @@ issuer-kbs --socket 127.0.0.1:50001 --insecure-http --auth-public-key config/pub Build and start KBS for resource distribution: ```shell -make passport-resource-kbs POLICY_ENGINE=opa +make passport-resource-kbs make install-resource-kbs resource-kbs --socket 127.0.0.1:50002 --insecure-http --auth-public-key config/public.pub ``` diff --git a/kbs/test/Makefile b/kbs/test/Makefile index ae67f77a4..6671fa0b8 100644 --- a/kbs/test/Makefile +++ b/kbs/test/Makefile @@ -82,12 +82,12 @@ install-dependencies: kbs: cd $(PROJECT_DIR) && \ - make background-check-kbs POLICY_ENGINE=opa && \ + make background-check-kbs && \ install -D --compare $(PROJECT_DIR)/../target/release/kbs $(CURDIR)/kbs resource-kbs: cd $(PROJECT_DIR) && \ - make passport-resource-kbs POLICY_ENGINE=opa && \ + make passport-resource-kbs && \ install -D --compare $(PROJECT_DIR)/../target/release/resource-kbs $(CURDIR)/resource-kbs client: diff --git a/kbs/test/config/kbs.toml b/kbs/test/config/kbs.toml index 7c6314ab5..f8ba5c400 100644 --- a/kbs/test/config/kbs.toml +++ b/kbs/test/config/kbs.toml @@ -1,31 +1,36 @@ +[http_server] sockets = ["127.0.0.1:8080"] -auth_public_key = "./work/kbs.pem" - private_key = "./work/https.key" certificate = "./work/https.crt" +insecure_http = false -[attestation_token_config] -trusted_certs_paths = ["./work/token-cert.pem"] +[attestation_token] +insecure_key = false +trusted_certs_paths = ["./work/ca-cert.pem"] -[repository_config] +[repository] type = "LocalFs" dir_path = "./work/repository" -[as_config] +[attestation_service] +type = "coco_as_builtin" work_dir = "./work/attestation-service" policy_engine = "opa" attestation_token_broker = "Simple" -[as_config.attestation_token_config] -duration_min = 5 + [attestation_service.attestation_token_config] + duration_min = 5 -[as_config.attestation_token_config.signer] -key_path = "./work/token.key" -cert_path = "./work/token-cert-chain.pem" + [attestation_service.attestation_token_config.signer] + key_path = "./work/token.key" + cert_path = "./work/token-cert-chain.pem" -[as_config.rvps_config] -store_type = "LocalFs" -remote_addr = "" + [attestation_service.rvps_config] + store_type = "LocalFs" + remote_addr = "" -[policy_engine_config] +[policy_engine] policy_path = "./work/kbs-policy.rego" + +[admin] +auth_public_key = "./work/kbs.pem" \ No newline at end of file diff --git a/kbs/test/config/resource-kbs.toml b/kbs/test/config/resource-kbs.toml index 8abbec27e..5b8ce37db 100644 --- a/kbs/test/config/resource-kbs.toml +++ b/kbs/test/config/resource-kbs.toml @@ -1,13 +1,17 @@ +[http_server] sockets = ["127.0.0.1:50002"] -auth_public_key = "./work/kbs.pem" insecure_http = true -[attestation_token_config] +[admin] +auth_public_key = "./work/kbs.pem" + +[attestation_token] trusted_certs_paths = ["./work/ca-cert.pem"] +insecure_key = false -[repository_config] +[repository] type = "LocalFs" dir_path = "./work/repository" -[policy_engine_config] +[policy_engine] policy_path = "./work/kbs-policy.rego" diff --git a/kbs/test_data/configs/coco-as-builtin-3.toml b/kbs/test_data/configs/coco-as-builtin-3.toml index 20d8df7b8..13186fe02 100644 --- a/kbs/test_data/configs/coco-as-builtin-3.toml +++ b/kbs/test_data/configs/coco-as-builtin-3.toml @@ -1,8 +1,7 @@ [http_server] insecure_http = true -[attestation_token_config] -type = "CoCo" +[attestation_token] [repository] type = "LocalFs" From d78dbe14d88f3ba607b2efc56a2a3090cf37d584 Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Fri, 27 Sep 2024 15:19:51 +0800 Subject: [PATCH 07/10] AS: reorder the dep in lexicographic order Signed-off-by: Xynnn007 --- attestation-service/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attestation-service/Cargo.toml b/attestation-service/Cargo.toml index 529fa6efb..237ce6d46 100644 --- a/attestation-service/Cargo.toml +++ b/attestation-service/Cargo.toml @@ -50,9 +50,9 @@ log.workspace = true openssl = "0.10.55" prost = { workspace = true, optional = true } rand = "0.8.5" -rsa = { version = "0.9.2", features = ["sha2"] } reference-value-provider-service = { path = "../rvps", optional = true } regorus.workspace = true +rsa = { version = "0.9.2", features = ["sha2"] } serde.workspace = true serde_json.workspace = true serde_variant = "0.1.2" From 4ef93f5a11059f092945c71d29fe5dd42cf2c303 Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Fri, 27 Sep 2024 15:21:34 +0800 Subject: [PATCH 08/10] KBS: change default feature to all backend AS and resource Now the KBS could be built with support for all backend ASes and enable one of them runtimely due to configuration file. Signed-off-by: Xynnn007 --- kbs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index 6414ebdc1..de2b4a48e 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -7,7 +7,7 @@ documentation.workspace = true edition.workspace = true [features] -default = ["coco-as-builtin", "resource", "opa"] +default = ["coco-as-builtin", "coco-as-grpc", "intel-trust-authority-as", "resource"] # Feature that allows to access resources from KBS resource = ["rsa", "reqwest", "aes-gcm", "jsonwebtoken"] From be3c836c33746a207e2cb68d1ec3d0cdddd92358 Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Wed, 30 Oct 2024 10:03:29 +0800 Subject: [PATCH 09/10] KBS: Use one API to serve both admin and user requests This patch uses a single logic to handle all different requests built upon plugin system. Also, resource module is by default a plugin and will be enabled by default. Signed-off-by: Xynnn007 --- kbs/Cargo.toml | 15 +- kbs/Makefile | 10 +- kbs/src/api_server.rs | 192 ++++++------------ kbs/src/attestation/config.rs | 31 +++ .../attestation/intel_trust_authority/mod.rs | 2 +- kbs/src/config.rs | 115 ++++------- kbs/src/error.rs | 4 - kbs/src/lib.rs | 3 - kbs/test/config/kbs.toml | 11 +- kbs/test/config/resource-kbs.toml | 9 +- kbs/test_data/configs/coco-as-builtin-3.toml | 11 +- kbs/test_data/configs/coco-as-grpc-1.toml | 13 +- kbs/test_data/configs/intel-ta-1.toml | 13 +- 13 files changed, 179 insertions(+), 250 deletions(-) diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index de2b4a48e..6121655d2 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -7,10 +7,7 @@ documentation.workspace = true edition.workspace = true [features] -default = ["coco-as-builtin", "coco-as-grpc", "intel-trust-authority-as", "resource"] - -# Feature that allows to access resources from KBS -resource = ["rsa", "reqwest", "aes-gcm", "jsonwebtoken"] +default = ["coco-as-builtin", "coco-as-grpc", "intel-trust-authority-as"] # Support a backend attestation service for KBS as = [] @@ -28,7 +25,7 @@ coco-as-builtin-no-verifier = ["coco-as", "attestation-service/rvps-builtin"] coco-as-grpc = ["coco-as", "mobc", "tonic", "tonic-build", "prost"] # Use Intel TA as backend attestation service -intel-trust-authority-as = ["as", "reqwest", "resource", "az-cvm-vtpm"] +intel-trust-authority-as = ["as", "az-cvm-vtpm"] # Use aliyun KMS as KBS backend aliyun = ["kms/aliyun"] @@ -36,7 +33,7 @@ aliyun = ["kms/aliyun"] [dependencies] actix-web = { workspace = true, features = ["openssl"] } actix-web-httpauth.workspace = true -aes-gcm = { version = "0.10.1", optional = true } +aes-gcm = "0.10.1" anyhow.workspace = true async-trait.workspace = true attestation-service = { path = "../attestation-service", default-features = false, optional = true } @@ -45,7 +42,7 @@ cfg-if.workspace = true clap = { workspace = true, features = ["derive", "env"] } config.workspace = true env_logger.workspace = true -jsonwebtoken = { workspace = true, default-features = false, optional = true } +jsonwebtoken = { workspace = true, default-features = false } jwt-simple.workspace = true kbs-types.workspace = true kms = { workspace = true, default-features = false } @@ -56,8 +53,8 @@ prost = { workspace = true, optional = true } rand = "0.8.5" regex = "1.11.1" regorus.workspace = true -reqwest = { workspace = true, features = ["json"], optional = true } -rsa = { version = "0.9.2", optional = true, features = ["sha2"] } +reqwest = { workspace = true, features = ["json"] } +rsa = { version = "0.9.2", features = ["sha2"] } scc = "2" semver = "1.0.16" serde = { workspace = true, features = ["derive"] } diff --git a/kbs/Makefile b/kbs/Makefile index 04652a016..b6f4d8804 100644 --- a/kbs/Makefile +++ b/kbs/Makefile @@ -16,9 +16,9 @@ COCO_AS_INTEGRATION_TYPE ?= builtin INSTALL_DESTDIR ?= /usr/local/bin ifeq ($(AS_TYPE), coco-as) - AS_FEATURE = $(AS_TYPE)-$(COCO_AS_INTEGRATION_TYPE) -else - AS_FEATURE = $(AS_TYPE) + AS_FEATURE += $(AS_TYPE)-$(COCO_AS_INTEGRATION_TYPE), +else ifneq ($(AS_TYPE), ) + AS_FEATURE += $(AS_TYPE), endif ifeq ($(ALIYUN), true) @@ -37,7 +37,7 @@ build: background-check-kbs .PHONY: background-check-kbs background-check-kbs: - cargo build -p kbs --locked --release --no-default-features --features $(AS_FEATURE),resource,$(FEATURES) + cargo build -p kbs --locked --release --no-default-features --features $(FEATURES),$(AS_FEATURE) .PHONY: passport-issuer-kbs passport-issuer-kbs: @@ -46,7 +46,7 @@ passport-issuer-kbs: .PHONY: passport-resource-kbs passport-resource-kbs: - cargo build -p kbs --locked --release --no-default-features --features resource,$(FEATURES) + cargo build -p kbs --locked --release --no-default-features --features $(FEATURES), mv ../target/release/kbs ../target/release/resource-kbs .PHONY: cli diff --git a/kbs/src/api_server.rs b/kbs/src/api_server.rs index 8d4f05772..694f5cdd0 100644 --- a/kbs/src/api_server.rs +++ b/kbs/src/api_server.rs @@ -12,7 +12,7 @@ use log::info; use crate::{ admin::Admin, config::KbsConfig, jwe::jwe, plugins::PluginManager, policy_engine::PolicyEngine, - resource::ResourceDesc, token::TokenVerifier, Error, Result, + token::TokenVerifier, Error, Result, }; const KBS_PREFIX: &str = "/kbs/v0"; @@ -28,9 +28,6 @@ macro_rules! kbs_path { pub struct ApiServer { plugin_manager: PluginManager, - #[cfg(feature = "resource")] - resource_storage: crate::resource::ResourceStorage, - #[cfg(feature = "as")] attestation_service: crate::attestation::AttestationService, @@ -61,15 +58,12 @@ impl ApiServer { } pub async fn new(config: KbsConfig) -> Result { - let plugin_manager = PluginManager::try_from(config.client_plugins.clone())?; + let plugin_manager = PluginManager::try_from(config.plugins.clone()) + .map_err(|e| Error::PluginManagerInitialization { source: e })?; let token_verifier = TokenVerifier::from_config(config.attestation_token.clone()).await?; let policy_engine = PolicyEngine::new(&config.policy_engine).await?; let admin_auth = Admin::try_from(config.admin.clone())?; - #[cfg(feature = "resource")] - let resource_storage = - crate::resource::ResourceStorage::try_from(config.repository.clone())?; - #[cfg(feature = "as")] let attestation_service = crate::attestation::AttestationService::new(config.attestation_service.clone()).await?; @@ -81,9 +75,6 @@ impl ApiServer { admin_auth, token_verifier, - #[cfg(feature = "resource")] - resource_storage, - #[cfg(feature = "as")] attestation_service, }) @@ -110,13 +101,8 @@ impl ApiServer { .app_data(web::Data::new(api_server)) .service( web::resource([kbs_path!("{plugin}{sub_path:.*}")]) - .route(web::get().to(client)) - .route(web::post().to(client)), - ) - .service( - web::resource([kbs_path!("admin/{plugin}/{sub_path:.*}")]) - .route(web::get().to(admin)) - .route(web::post().to(admin)), + .route(web::get().to(api)) + .route(web::post().to(api)), ) } }); @@ -145,8 +131,8 @@ impl ApiServer { } } -/// Client APIs. /kbs/v0/XXX -pub(crate) async fn client( +/// APIs +pub(crate) async fn api( request: HttpRequest, body: web::Bytes, core: web::Data, @@ -182,136 +168,84 @@ pub(crate) async fn client( .map_err(From::from), #[cfg(feature = "as")] "attestation-policy" if request.method() == Method::POST => { - core.admin_auth.validate_auth(&request)?; - core.attestation_service.set_policy(&body).await?; Ok(HttpResponse::Ok().finish()) } + // TODO: consider to rename the api name for it is not only for + // resource retrievement but for all plugins. "resource-policy" if request.method() == Method::POST => { core.admin_auth.validate_auth(&request)?; - core.policy_engine.set_policy(&body).await?; Ok(HttpResponse::Ok().finish()) } - #[cfg(feature = "resource")] - "resource" => { - if request.method() == Method::GET { - // Resource APIs needs to be authorized by the Token and policy - let resource_desc = - sub_path - .strip_prefix('/') - .ok_or(Error::IllegalAccessedPath { - path: end_point.clone(), - })?; - - let token = core - .get_attestation_token(&request) - .await - .map_err(|_| Error::TokenNotFound)?; - - let claims = core.token_verifier.verify(token).await?; - - let claim_str = serde_json::to_string(&claims)?; - if !core - .policy_engine - .evaluate(resource_desc, &claim_str) - .await? - { - return Err(Error::PolicyDeny); - }; - - let resource_description = ResourceDesc::try_from(resource_desc)?; - let resource = core - .resource_storage - .get_secret_resource(resource_description) - .await?; - - let public_key = core.token_verifier.extract_tee_public_key(claims)?; - let jwe = jwe(public_key, resource).map_err(|e| Error::JweError { source: e })?; - - let res = serde_json::to_string(&jwe)?; - - Ok(HttpResponse::Ok() - .content_type("application/json") - .body(res)) - } else if request.method() == Method::POST { - let resource_desc = - sub_path - .strip_prefix('/') - .ok_or(Error::IllegalAccessedPath { - path: end_point.clone(), - })?; - let resource_description = ResourceDesc::try_from(resource_desc)?; - core.admin_auth.validate_auth(&request)?; - core.resource_storage - .set_secret_resource(resource_description, &body) - .await?; + // TODO: consider to rename the api name for it is not only for + // resource retrievement but for all plugins. + "resource-policy" if request.method() == Method::GET => { + core.admin_auth.validate_auth(&request)?; + let policy = core.policy_engine.get_policy().await?; - Ok(HttpResponse::Ok().content_type("application/json").body("")) - } else { - Ok(HttpResponse::NotImplemented() - .content_type("application/json") - .body("")) - } + Ok(HttpResponse::Ok().content_type("text/xml").body(policy)) } plugin_name => { - // Plugin calls needs to be authorized by the Token and policy - let token = core - .get_attestation_token(&request) - .await - .map_err(|_| Error::TokenNotFound)?; - - let claims = core.token_verifier.verify(token).await?; - - let claim_str = serde_json::to_string(&claims)?; - - // TODO: add policy filter support for other plugins - if !core.policy_engine.evaluate(&end_point, &claim_str).await? { - return Err(Error::PolicyDeny); - } - let plugin = core .plugin_manager .get(plugin_name) .ok_or(Error::PluginNotFound { plugin_name: plugin_name.to_string(), })?; + let body = body.to_vec(); - let response = plugin - .handle(body, query.into(), sub_path.into(), request.method()) - .await?; - Ok(response) - } - } -} + if plugin + .validate_auth(&body, query, sub_path, request.method()) + .await + .map_err(|e| Error::PluginInternalError { source: e })? + { + // Plugin calls needs to be authorized by the admin auth + core.admin_auth.validate_auth(&request)?; + let response = plugin + .handle(&body, query, sub_path, request.method()) + .await + .map_err(|e| Error::PluginInternalError { source: e })?; -/// Admin APIs. -pub(crate) async fn admin( - request: HttpRequest, - _body: web::Bytes, - core: web::Data, -) -> Result { - // Admin APIs needs to be authorized by the admin asymmetric key - core.admin_auth.validate_auth(&request)?; + Ok(HttpResponse::Ok().content_type("text/xml").body(response)) + } else { + // Plugin calls needs to be authorized by the Token and policy + let token = core + .get_attestation_token(&request) + .await + .map_err(|_| Error::TokenNotFound)?; - let plugin_name = request - .match_info() - .get("plugin") - .ok_or(Error::IllegalAccessedPath { - path: request.path().to_string(), - })?; - let sub_path = request - .match_info() - .get("sub_path") - .ok_or(Error::IllegalAccessedPath { - path: request.path().to_string(), - })?; + let claims = core.token_verifier.verify(token).await?; + + let claim_str = serde_json::to_string(&claims)?; - info!("Admin plugin {plugin_name} with path {sub_path} called"); + // TODO: add policy filter support for other plugins + if !core.policy_engine.evaluate(&end_point, &claim_str).await? { + return Err(Error::PolicyDeny); + } - // TODO: add admin path handlers - let response = HttpResponse::NotFound().body("no admin plugin found"); - Ok(response) + let response = plugin + .handle(&body, query, sub_path, request.method()) + .await + .map_err(|e| Error::PluginInternalError { source: e })?; + if plugin + .encrypted(&body, query, sub_path, request.method()) + .await + .map_err(|e| Error::PluginInternalError { source: e })? + { + let public_key = core.token_verifier.extract_tee_public_key(claims)?; + let jwe = + jwe(public_key, response).map_err(|e| Error::JweError { source: e })?; + let res = serde_json::to_string(&jwe)?; + return Ok(HttpResponse::Ok() + .content_type("application/json") + .body(res)); + } + + Ok(HttpResponse::Ok().content_type("text/xml").body(response)) + } + } + } } diff --git a/kbs/src/attestation/config.rs b/kbs/src/attestation/config.rs index f49be5a3b..532548de6 100644 --- a/kbs/src/attestation/config.rs +++ b/kbs/src/attestation/config.rs @@ -4,14 +4,31 @@ use serde::Deserialize; +pub const DEFAULT_TIMEOUT: i64 = 5; + #[derive(Clone, Debug, Deserialize, PartialEq)] pub struct AttestationConfig { #[serde(flatten)] + #[serde(default)] pub attestation_service: AttestationServiceConfig, + #[serde(default = "default_timeout")] pub timeout: i64, } +impl Default for AttestationConfig { + fn default() -> Self { + Self { + attestation_service: AttestationServiceConfig::default(), + timeout: DEFAULT_TIMEOUT, + } + } +} + +fn default_timeout() -> i64 { + DEFAULT_TIMEOUT +} + #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(tag = "type")] pub enum AttestationServiceConfig { @@ -27,3 +44,17 @@ pub enum AttestationServiceConfig { #[serde(alias = "intel_ta")] IntelTA(super::intel_trust_authority::IntelTrustAuthorityConfig), } + +impl Default for AttestationServiceConfig { + fn default() -> Self { + cfg_if::cfg_if! { + if #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] { + AttestationServiceConfig::CoCoASBuiltIn(attestation_service::config::Config::default()) + } else if #[cfg(feature = "coco-as-grpc")] { + AttestationServiceConfig::CoCoASGrpc(super::coco::grpc::GrpcConfig::default()) + } else { + AttestationServiceConfig::IntelTA(super::intel_trust_authority::IntelTrustAuthorityConfig::default()) + } + } + } +} diff --git a/kbs/src/attestation/intel_trust_authority/mod.rs b/kbs/src/attestation/intel_trust_authority/mod.rs index 9cc9a073d..9dd910acc 100644 --- a/kbs/src/attestation/intel_trust_authority/mod.rs +++ b/kbs/src/attestation/intel_trust_authority/mod.rs @@ -76,7 +76,7 @@ struct ErrorResponse { error: String, } -#[derive(Clone, Debug, Deserialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, PartialEq, Default)] pub struct IntelTrustAuthorityConfig { pub base_url: String, pub api_key: String, diff --git a/kbs/src/config.rs b/kbs/src/config.rs index b145b4af6..f02233e35 100644 --- a/kbs/src/config.rs +++ b/kbs/src/config.rs @@ -15,7 +15,6 @@ use std::path::{Path, PathBuf}; const DEFAULT_INSECURE_HTTP: bool = false; const DEFAULT_SOCKET: &str = "127.0.0.1:8080"; -const DEFAULT_TIMEOUT: i64 = 5; #[derive(Clone, Debug, Deserialize, PartialEq)] pub struct HttpServerConfig { @@ -47,17 +46,13 @@ impl Default for HttpServerConfig { /// Contains all configurable KBS properties. #[derive(Debug, Clone, Deserialize, PartialEq)] pub struct KbsConfig { - /// Resource repository config. - #[cfg(feature = "resource")] - #[serde(default)] - pub repository: crate::resource::RepositoryConfig, - /// Attestation token result broker config. #[serde(default)] pub attestation_token: AttestationTokenVerifierConfig, /// Configuration for the Attestation Service. #[cfg(feature = "as")] + #[serde(default)] pub attestation_service: crate::attestation::config::AttestationConfig, /// Configuration for the KBS Http Server @@ -72,7 +67,7 @@ pub struct KbsConfig { pub policy_engine: PolicyEngineConfig, #[serde(default)] - pub client_plugins: Vec, + pub plugins: Vec, } impl TryFrom<&Path> for KbsConfig { @@ -85,7 +80,6 @@ impl TryFrom<&Path> for KbsConfig { .set_default("admin.insecure_api", DEFAULT_INSECURE_API)? .set_default("http_server.insecure_http", DEFAULT_INSECURE_HTTP)? .set_default("http_server.sockets", vec![DEFAULT_SOCKET])? - .set_default("attestation_service.timeout", DEFAULT_TIMEOUT)? .add_source(File::with_name(config_path.to_str().unwrap())) .build()?; @@ -110,11 +104,13 @@ mod tests { use crate::{ admin::config::AdminConfig, - config::{ - HttpServerConfig, DEFAULT_INSECURE_API, DEFAULT_INSECURE_HTTP, DEFAULT_SOCKET, - DEFAULT_TIMEOUT, + config::{HttpServerConfig, DEFAULT_INSECURE_API, DEFAULT_INSECURE_HTTP, DEFAULT_SOCKET}, + plugins::{ + implementations::{ + resource::local_fs::LocalFsRepoDesc, RepositoryConfig, SampleConfig, + }, + PluginsConfig, }, - plugins::{sample::SampleConfig, PluginsConfig}, policy_engine::{PolicyEngineConfig, DEFAULT_POLICY_PATH}, token::AttestationTokenVerifierConfig, }; @@ -135,12 +131,6 @@ mod tests { #[rstest] #[case("test_data/configs/coco-as-grpc-1.toml", KbsConfig { - #[cfg(feature = "resource")] - repository: crate::resource::RepositoryConfig::LocalFs( - crate::resource::local_fs::LocalFsRepoDesc { - dir_path: "/tmp/kbs-resource".into(), - }, - ), attestation_token: AttestationTokenVerifierConfig { trusted_certs_paths: vec!["/etc/ca".into(), "/etc/ca2".into()], insecure_key: false, @@ -171,17 +161,16 @@ mod tests { policy_engine: PolicyEngineConfig { policy_path: PathBuf::from("/etc/kbs-policy.rego"), }, - client_plugins: vec![PluginsConfig::Sample(SampleConfig { + plugins: vec![PluginsConfig::Sample(SampleConfig { item: "value1".into(), - })], + }), + PluginsConfig::ResourceStorage(RepositoryConfig::LocalFs( + LocalFsRepoDesc { + dir_path: "/tmp/kbs-resource".into(), + }, + ))], })] #[case("test_data/configs/coco-as-builtin-1.toml", KbsConfig { - #[cfg(feature = "resource")] - repository: crate::resource::RepositoryConfig::LocalFs( - crate::resource::local_fs::LocalFsRepoDesc { - dir_path: DEFAULT_REPO_DIR_PATH.into(), - }, - ), attestation_token: AttestationTokenVerifierConfig { trusted_certs_paths: vec![], insecure_key: false, @@ -208,7 +197,7 @@ mod tests { }, } ), - timeout: DEFAULT_TIMEOUT, + timeout: crate::attestation::config::DEFAULT_TIMEOUT, }, http_server: HttpServerConfig { sockets: vec![DEFAULT_SOCKET.parse().unwrap()], @@ -223,15 +212,9 @@ mod tests { policy_engine: PolicyEngineConfig { policy_path: DEFAULT_POLICY_PATH.into(), }, - client_plugins: vec![], + plugins: Vec::new(), })] #[case("test_data/configs/intel-ta-1.toml", KbsConfig { - #[cfg(feature = "resource")] - repository: crate::resource::RepositoryConfig::LocalFs( - crate::resource::local_fs::LocalFsRepoDesc { - dir_path: "/tmp/kbs-resource".into(), - }, - ), attestation_token: AttestationTokenVerifierConfig { trusted_jwk_sets: vec!["/etc/ca".into(), "/etc/ca2".into()], insecure_key: false, @@ -249,7 +232,7 @@ mod tests { allow_unmatched_policy: Some(true), } ), - timeout: DEFAULT_TIMEOUT, + timeout: crate::attestation::config::DEFAULT_TIMEOUT, }, http_server: HttpServerConfig { sockets: vec!["0.0.0.0:8080".parse().unwrap()], @@ -264,13 +247,16 @@ mod tests { policy_engine: PolicyEngineConfig { policy_path: PathBuf::from("/etc/kbs-policy.rego"), }, - client_plugins: vec![PluginsConfig::Sample(SampleConfig { + plugins: vec![PluginsConfig::Sample(SampleConfig { item: "value1".into(), - })], + }), + PluginsConfig::ResourceStorage(RepositoryConfig::LocalFs( + LocalFsRepoDesc { + dir_path: "/tmp/kbs-resource".into(), + }, + ))], })] #[case("test_data/configs/coco-as-grpc-2.toml", KbsConfig { - #[cfg(feature = "resource")] - repository: crate::resource::RepositoryConfig::default(), attestation_token: AttestationTokenVerifierConfig { ..Default::default() }, @@ -283,7 +269,7 @@ mod tests { pool_size: crate::attestation::coco::grpc::DEFAULT_POOL_SIZE, }, ), - timeout: DEFAULT_TIMEOUT, + timeout: crate::attestation::config::DEFAULT_TIMEOUT, }, http_server: HttpServerConfig { sockets: vec!["0.0.0.0:8080".parse().unwrap()], @@ -296,15 +282,9 @@ mod tests { insecure_api: DEFAULT_INSECURE_API, }, policy_engine: PolicyEngineConfig::default(), - client_plugins: Vec::default(), + plugins: Vec::new(), })] #[case("test_data/configs/coco-as-builtin-2.toml", KbsConfig { - #[cfg(feature = "resource")] - repository: crate::resource::RepositoryConfig::LocalFs( - crate::resource::local_fs::LocalFsRepoDesc { - dir_path: DEFAULT_REPO_DIR_PATH.into(), - }, - ), attestation_token: AttestationTokenVerifierConfig { trusted_certs_paths: vec![], insecure_key: false, @@ -330,7 +310,7 @@ mod tests { }, } ), - timeout: DEFAULT_TIMEOUT, + timeout: crate::attestation::config::DEFAULT_TIMEOUT, }, http_server: HttpServerConfig { sockets: vec!["0.0.0.0:8080".parse().unwrap()], @@ -343,7 +323,7 @@ mod tests { insecure_api: DEFAULT_INSECURE_API, }, policy_engine: PolicyEngineConfig::default(), - client_plugins: vec![], + plugins: Vec::new(), })] #[case("test_data/configs/intel-ta-2.toml", KbsConfig { attestation_token: AttestationTokenVerifierConfig { @@ -363,7 +343,7 @@ mod tests { allow_unmatched_policy: None, } ), - timeout: DEFAULT_TIMEOUT, + timeout: crate::attestation::config::DEFAULT_TIMEOUT, }, http_server: HttpServerConfig { sockets: vec!["0.0.0.0:8080".parse().unwrap()], @@ -376,15 +356,9 @@ mod tests { insecure_api: DEFAULT_INSECURE_API, }, policy_engine: PolicyEngineConfig::default(), - client_plugins: vec![], - #[cfg(feature = "resource")] - repository: crate::resource::RepositoryConfig::LocalFs( - crate::resource::local_fs::LocalFsRepoDesc::default(), - ), + plugins: Vec::new(), })] #[case("test_data/configs/coco-as-grpc-3.toml", KbsConfig { - #[cfg(feature = "resource")] - repository: crate::resource::RepositoryConfig::default(), attestation_token: AttestationTokenVerifierConfig { ..Default::default() }, @@ -397,7 +371,7 @@ mod tests { pool_size: 100, }, ), - timeout: DEFAULT_TIMEOUT, + timeout: crate::attestation::config::DEFAULT_TIMEOUT, }, http_server: HttpServerConfig { insecure_http: true, @@ -408,7 +382,7 @@ mod tests { ..Default::default() }, policy_engine: PolicyEngineConfig::default(), - client_plugins: Vec::default(), + plugins: Vec::new(), })] #[case("test_data/configs/intel-ta-3.toml", KbsConfig { attestation_token: AttestationTokenVerifierConfig { @@ -428,7 +402,7 @@ mod tests { allow_unmatched_policy: None, } ), - timeout: DEFAULT_TIMEOUT, + timeout: crate::attestation::config::DEFAULT_TIMEOUT, }, http_server: HttpServerConfig { insecure_http: true, @@ -439,19 +413,9 @@ mod tests { ..Default::default() }, policy_engine: PolicyEngineConfig::default(), - client_plugins: vec![], - #[cfg(feature = "resource")] - repository: crate::resource::RepositoryConfig::LocalFs( - crate::resource::local_fs::LocalFsRepoDesc::default(), - ), + plugins: Vec::new(), })] #[case("test_data/configs/coco-as-builtin-3.toml", KbsConfig { - #[cfg(feature = "resource")] - repository: crate::resource::RepositoryConfig::LocalFs( - crate::resource::local_fs::LocalFsRepoDesc { - dir_path: "/opt/confidential-containers/kbs/repository".into(), - }, - ), attestation_token: AttestationTokenVerifierConfig { trusted_certs_paths: vec![], insecure_key: false, @@ -477,7 +441,7 @@ mod tests { }, } ), - timeout: DEFAULT_TIMEOUT, + timeout: crate::attestation::config::DEFAULT_TIMEOUT, }, http_server: HttpServerConfig { insecure_http: true, @@ -490,7 +454,12 @@ mod tests { policy_engine: PolicyEngineConfig { policy_path: "/opa/confidential-containers/kbs/policy.rego".into(), }, - client_plugins: vec![], + plugins: vec![ + PluginsConfig::ResourceStorage(RepositoryConfig::LocalFs( + LocalFsRepoDesc { + dir_path: "/opt/confidential-containers/kbs/repository".into(), + }, + ))], })] fn read_config(#[case] config_path: &str, #[case] expected: KbsConfig) { let config = KbsConfig::try_from(Path::new(config_path)).unwrap(); diff --git a/kbs/src/error.rs b/kbs/src/error.rs index 5cdaf6886..cfc09f18f 100644 --- a/kbs/src/error.rs +++ b/kbs/src/error.rs @@ -67,10 +67,6 @@ pub enum Error { #[error("Policy engine error")] PolicyEngine(#[from] crate::policy_engine::KbsPolicyEngineError), - #[cfg(feature = "resource")] - #[error("Resource access failed")] - ResourceAccessFailed(#[from] crate::resource::Error), - #[error("Serialize/Deserialize failed")] SerdeError(#[from] serde_json::Error), diff --git a/kbs/src/lib.rs b/kbs/src/lib.rs index d11e0edcd..818e21f4c 100644 --- a/kbs/src/lib.rs +++ b/kbs/src/lib.rs @@ -7,9 +7,6 @@ #[cfg(feature = "as")] pub mod attestation; -#[cfg(feature = "resource")] -mod resource; - /// KBS config pub mod config; pub use config::KbsConfig; diff --git a/kbs/test/config/kbs.toml b/kbs/test/config/kbs.toml index f8ba5c400..d5acea1b3 100644 --- a/kbs/test/config/kbs.toml +++ b/kbs/test/config/kbs.toml @@ -8,10 +8,6 @@ insecure_http = false insecure_key = false trusted_certs_paths = ["./work/ca-cert.pem"] -[repository] -type = "LocalFs" -dir_path = "./work/repository" - [attestation_service] type = "coco_as_builtin" work_dir = "./work/attestation-service" @@ -33,4 +29,9 @@ attestation_token_broker = "Simple" policy_path = "./work/kbs-policy.rego" [admin] -auth_public_key = "./work/kbs.pem" \ No newline at end of file +auth_public_key = "./work/kbs.pem" + +[[plugins]] +name = "resource" +type = "LocalFs" +dir_path = "./work/repository" \ No newline at end of file diff --git a/kbs/test/config/resource-kbs.toml b/kbs/test/config/resource-kbs.toml index 5b8ce37db..99ef258e7 100644 --- a/kbs/test/config/resource-kbs.toml +++ b/kbs/test/config/resource-kbs.toml @@ -9,9 +9,10 @@ auth_public_key = "./work/kbs.pem" trusted_certs_paths = ["./work/ca-cert.pem"] insecure_key = false -[repository] -type = "LocalFs" -dir_path = "./work/repository" - [policy_engine] policy_path = "./work/kbs-policy.rego" + +[[plugins]] +name = "resource" +type = "LocalFs" +dir_path = "./work/repository" \ No newline at end of file diff --git a/kbs/test_data/configs/coco-as-builtin-3.toml b/kbs/test_data/configs/coco-as-builtin-3.toml index 13186fe02..003dc0f13 100644 --- a/kbs/test_data/configs/coco-as-builtin-3.toml +++ b/kbs/test_data/configs/coco-as-builtin-3.toml @@ -3,10 +3,6 @@ insecure_http = true [attestation_token] -[repository] -type = "LocalFs" -dir_path = "/opt/confidential-containers/kbs/repository" - [attestation_service] type = "coco_as_builtin" work_dir = "/opt/confidential-containers/attestation-service" @@ -24,4 +20,9 @@ attestation_token_broker = "Simple" policy_path = "/opa/confidential-containers/kbs/policy.rego" [admin] -insecure_api = true \ No newline at end of file +insecure_api = true + +[[plugins]] +name = "resource" +type = "LocalFs" +dir_path = "/opt/confidential-containers/kbs/repository" \ No newline at end of file diff --git a/kbs/test_data/configs/coco-as-grpc-1.toml b/kbs/test_data/configs/coco-as-grpc-1.toml index d9e3f8d9c..addeb00de 100644 --- a/kbs/test_data/configs/coco-as-grpc-1.toml +++ b/kbs/test_data/configs/coco-as-grpc-1.toml @@ -1,7 +1,3 @@ -[repository] -type = "LocalFs" -dir_path = "/tmp/kbs-resource" - [attestation_token] trusted_certs_paths = ["/etc/ca", "/etc/ca2"] @@ -24,6 +20,11 @@ insecure_api = false [policy_engine] policy_path = "/etc/kbs-policy.rego" -[[client_plugins]] +[[plugins]] name = "sample" -item = "value1" \ No newline at end of file +item = "value1" + +[[plugins]] +name = "resource" +type = "LocalFs" +dir_path = "/tmp/kbs-resource" \ No newline at end of file diff --git a/kbs/test_data/configs/intel-ta-1.toml b/kbs/test_data/configs/intel-ta-1.toml index 281b1ea40..68a85ba89 100644 --- a/kbs/test_data/configs/intel-ta-1.toml +++ b/kbs/test_data/configs/intel-ta-1.toml @@ -1,7 +1,3 @@ -[repository] -type = "LocalFs" -dir_path = "/tmp/kbs-resource" - [attestation_token] trusted_jwk_sets = ["/etc/ca", "/etc/ca2"] @@ -25,6 +21,11 @@ insecure_api = false [policy_engine] policy_path = "/etc/kbs-policy.rego" -[[client_plugins]] +[[plugins]] name = "sample" -item = "value1" \ No newline at end of file +item = "value1" + +[[plugins]] +name = "resource" +type = "LocalFs" +dir_path = "/tmp/kbs-resource" \ No newline at end of file From 85b7ab84a63e3621358ee9cb2240895d5ff25cd6 Mon Sep 17 00:00:00 2001 From: Xynnn007 Date: Tue, 29 Oct 2024 15:46:08 +0800 Subject: [PATCH 10/10] toml: add extra line to all toml files Signed-off-by: Xynnn007 --- kbs/config/docker-compose/kbs-config.toml | 2 +- kbs/config/kbs-config-grpc.toml | 2 +- kbs/config/kbs-config.toml | 2 +- kbs/config/kubernetes/base/kbs-config.toml | 2 +- kbs/config/kubernetes/ita/kbs-config.toml | 2 +- kbs/test/config/kbs.toml | 2 +- kbs/test/config/resource-kbs.toml | 2 +- kbs/test_data/configs/coco-as-builtin-2.toml | 2 +- kbs/test_data/configs/coco-as-builtin-3.toml | 2 +- kbs/test_data/configs/coco-as-grpc-1.toml | 2 +- kbs/test_data/configs/coco-as-grpc-2.toml | 2 +- kbs/test_data/configs/coco-as-grpc-3.toml | 2 +- kbs/test_data/configs/intel-ta-1.toml | 2 +- kbs/test_data/configs/intel-ta-2.toml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/kbs/config/docker-compose/kbs-config.toml b/kbs/config/docker-compose/kbs-config.toml index 9bb770fc9..bf9b6157f 100644 --- a/kbs/config/docker-compose/kbs-config.toml +++ b/kbs/config/docker-compose/kbs-config.toml @@ -15,4 +15,4 @@ auth_public_key = "/opt/confidential-containers/kbs/user-keys/public.pub" [[plugins]] name = "resource" type = "LocalFs" -dir_path = "/opt/confidential-containers/kbs/repository" \ No newline at end of file +dir_path = "/opt/confidential-containers/kbs/repository" diff --git a/kbs/config/kbs-config-grpc.toml b/kbs/config/kbs-config-grpc.toml index 45d2c0865..8bdc818da 100644 --- a/kbs/config/kbs-config-grpc.toml +++ b/kbs/config/kbs-config-grpc.toml @@ -10,4 +10,4 @@ as_addr = "http://127.0.0.1:50004" pool_size = 200 [admin] -insecure_api = true \ No newline at end of file +insecure_api = true diff --git a/kbs/config/kbs-config.toml b/kbs/config/kbs-config.toml index b51069608..a42fb7d89 100644 --- a/kbs/config/kbs-config.toml +++ b/kbs/config/kbs-config.toml @@ -25,4 +25,4 @@ attestation_token_broker = "Simple" policy_path = "/opa/confidential-containers/kbs/policy.rego" [admin] -insecure_api = true \ No newline at end of file +insecure_api = true diff --git a/kbs/config/kubernetes/base/kbs-config.toml b/kbs/config/kubernetes/base/kbs-config.toml index 5256b8c8d..489cfdf97 100644 --- a/kbs/config/kubernetes/base/kbs-config.toml +++ b/kbs/config/kubernetes/base/kbs-config.toml @@ -21,4 +21,4 @@ attestation_token_broker = "Simple" remote_addr = "" [admin] -auth_public_key = "/kbs/kbs.pem" \ No newline at end of file +auth_public_key = "/kbs/kbs.pem" diff --git a/kbs/config/kubernetes/ita/kbs-config.toml b/kbs/config/kubernetes/ita/kbs-config.toml index ef942819c..37eefb727 100644 --- a/kbs/config/kubernetes/ita/kbs-config.toml +++ b/kbs/config/kubernetes/ita/kbs-config.toml @@ -14,4 +14,4 @@ api_key = "tBfd5kKX2x9ahbodKV1..." certs_file = "https://portal.trustauthority.intel.com" [admin] -auth_public_key = "/kbs/kbs.pem" \ No newline at end of file +auth_public_key = "/kbs/kbs.pem" diff --git a/kbs/test/config/kbs.toml b/kbs/test/config/kbs.toml index d5acea1b3..933f16979 100644 --- a/kbs/test/config/kbs.toml +++ b/kbs/test/config/kbs.toml @@ -34,4 +34,4 @@ auth_public_key = "./work/kbs.pem" [[plugins]] name = "resource" type = "LocalFs" -dir_path = "./work/repository" \ No newline at end of file +dir_path = "./work/repository" diff --git a/kbs/test/config/resource-kbs.toml b/kbs/test/config/resource-kbs.toml index 99ef258e7..977ef10c6 100644 --- a/kbs/test/config/resource-kbs.toml +++ b/kbs/test/config/resource-kbs.toml @@ -15,4 +15,4 @@ policy_path = "./work/kbs-policy.rego" [[plugins]] name = "resource" type = "LocalFs" -dir_path = "./work/repository" \ No newline at end of file +dir_path = "./work/repository" diff --git a/kbs/test_data/configs/coco-as-builtin-2.toml b/kbs/test_data/configs/coco-as-builtin-2.toml index b5e1006a5..c2398128c 100644 --- a/kbs/test_data/configs/coco-as-builtin-2.toml +++ b/kbs/test_data/configs/coco-as-builtin-2.toml @@ -20,4 +20,4 @@ attestation_token_broker = "Simple" remote_addr = "" [admin] -auth_public_key = "/kbs/kbs.pem" \ No newline at end of file +auth_public_key = "/kbs/kbs.pem" diff --git a/kbs/test_data/configs/coco-as-builtin-3.toml b/kbs/test_data/configs/coco-as-builtin-3.toml index 003dc0f13..1d8e13f44 100644 --- a/kbs/test_data/configs/coco-as-builtin-3.toml +++ b/kbs/test_data/configs/coco-as-builtin-3.toml @@ -25,4 +25,4 @@ insecure_api = true [[plugins]] name = "resource" type = "LocalFs" -dir_path = "/opt/confidential-containers/kbs/repository" \ No newline at end of file +dir_path = "/opt/confidential-containers/kbs/repository" diff --git a/kbs/test_data/configs/coco-as-grpc-1.toml b/kbs/test_data/configs/coco-as-grpc-1.toml index addeb00de..4a6067a0c 100644 --- a/kbs/test_data/configs/coco-as-grpc-1.toml +++ b/kbs/test_data/configs/coco-as-grpc-1.toml @@ -27,4 +27,4 @@ item = "value1" [[plugins]] name = "resource" type = "LocalFs" -dir_path = "/tmp/kbs-resource" \ No newline at end of file +dir_path = "/tmp/kbs-resource" diff --git a/kbs/test_data/configs/coco-as-grpc-2.toml b/kbs/test_data/configs/coco-as-grpc-2.toml index a0aedc0d5..dc5c1fdbd 100644 --- a/kbs/test_data/configs/coco-as-grpc-2.toml +++ b/kbs/test_data/configs/coco-as-grpc-2.toml @@ -9,4 +9,4 @@ type = "coco_as_grpc" as_addr = "http://as:50004" [admin] -auth_public_key = "/opt/confidential-containers/kbs/user-keys/public.pub" \ No newline at end of file +auth_public_key = "/opt/confidential-containers/kbs/user-keys/public.pub" diff --git a/kbs/test_data/configs/coco-as-grpc-3.toml b/kbs/test_data/configs/coco-as-grpc-3.toml index c6d8e61e3..0ba9958a0 100644 --- a/kbs/test_data/configs/coco-as-grpc-3.toml +++ b/kbs/test_data/configs/coco-as-grpc-3.toml @@ -9,4 +9,4 @@ as_addr = "http://127.0.0.1:50004" pool_size = 100 [admin] -insecure_api = true \ No newline at end of file +insecure_api = true diff --git a/kbs/test_data/configs/intel-ta-1.toml b/kbs/test_data/configs/intel-ta-1.toml index 68a85ba89..84a4125d4 100644 --- a/kbs/test_data/configs/intel-ta-1.toml +++ b/kbs/test_data/configs/intel-ta-1.toml @@ -28,4 +28,4 @@ item = "value1" [[plugins]] name = "resource" type = "LocalFs" -dir_path = "/tmp/kbs-resource" \ No newline at end of file +dir_path = "/tmp/kbs-resource" diff --git a/kbs/test_data/configs/intel-ta-2.toml b/kbs/test_data/configs/intel-ta-2.toml index 3c7714430..e4b40b73d 100644 --- a/kbs/test_data/configs/intel-ta-2.toml +++ b/kbs/test_data/configs/intel-ta-2.toml @@ -14,4 +14,4 @@ api_key = "tBfd5kKX2x9ahbodKV1..." certs_file = "https://portal.trustauthority.intel.com" [admin] -auth_public_key = "/kbs/kbs.pem" \ No newline at end of file +auth_public_key = "/kbs/kbs.pem"