-
Notifications
You must be signed in to change notification settings - Fork 82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
KBS | Refactoring the codebase / update config file format / bring in plugin mechanism #514
Open
Xynnn007
wants to merge
13
commits into
confidential-containers:main
Choose a base branch
from
Xynnn007:refactor-kbs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,699
−2,101
Open
Changes from 1 commit
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ff2e518
KBS: refactor attestation module
Xynnn007 542f64c
KBS: combine CoCo Token and Jwk Token verifier
Xynnn007 a271f53
KBS: refactor policy engine module
Xynnn007 4d89589
KBS: add Admin auth module
Xynnn007 a6cf383
KBS: add resource module
Xynnn007 9f08369
KBS: add Plugins module
Xynnn007 1d2af16
KBS: Use new launch Config
Xynnn007 8e83068
KBS: fix CI and exampled configurations
Xynnn007 823cc81
AS: reorder the dep in lexicographic order
Xynnn007 82f4118
KBS: change default feature to all backend AS and resource
Xynnn007 db46ea2
KBS: move all admin APIs under /kbs/v0/admin
Xynnn007 bc176e4
fixup! KBS: add resource module
Xynnn007 c058c6a
KBS: abondon admin API and make resource a plugin
Xynnn007 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> { | ||
let mut nonce: Vec<u8> = 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<Challenge> { | ||
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<String>; | ||
|
||
/// 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<Challenge> { | ||
generic_generate_challenge(tee, tee_parameters).await | ||
} | ||
} | ||
|
||
/// Attestation Service | ||
#[derive(Clone)] | ||
pub struct AttestationService { | ||
/// Attestation Module | ||
inner: Arc<dyn Attest>, | ||
|
||
/// A concurrent safe map to keep status of RCAR status | ||
session_map: Arc<SessionMap>, | ||
|
||
/// Max timeout between `auth` and `attest` request of a client | ||
timeout: i64, | ||
} | ||
|
||
#[derive(Deserialize, Debug)] | ||
pub struct SetPolicyInput { | ||
policy_id: String, | ||
policy: String, | ||
} | ||
|
||
impl AttestationService { | ||
pub async fn new(config: AttestationConfig) -> Result<Self> { | ||
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<HttpResponse> { | ||
self.__auth(request) | ||
.await | ||
.map_err(|e| Error::RcarAuthFailed { source: e }) | ||
} | ||
|
||
async fn __auth(&self, request: &[u8]) -> anyhow::Result<HttpResponse> { | ||
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<HttpResponse> { | ||
self.__attest(attestation, request) | ||
.await | ||
.map_err(|e| Error::RcarAttestFailed { source: e }) | ||
} | ||
|
||
async fn __attest( | ||
&self, | ||
attestation: &[u8], | ||
request: HttpRequest, | ||
) -> anyhow::Result<HttpResponse> { | ||
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<String> { | ||
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this convention of creating "private functions w/ underscore prefixes doesn't look like idiomatic rust. can we inline
__myfunc()
inmyfunc()
or if they need to be separated can we put it into a module?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean to use
#[inline]
uponpub async fn attest(...)
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, just move the __attest code into the attest function (unless there is a reason to keep them apart, then we can move it it into a module maybe)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh. The wrapping is for error conversion. If move all logic from
__attest
toattest
, we need a lot ofmap_err
fromanyhow::Error
toError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if
Error
is usingthiserror
macros, we should be able to use aMisc(#[from] anyhow::Error)
entry in the error enum, or it even works automatically, I don't rememberThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. The
#[from]
could automatically convert ALLanyhow:Error
toError
with?
. In this case we want to do some classfication upon the differentanyhow::Error
due toAuth
andAttest
of RCARThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, if we want to mix anyhow and concrete error types we have to resort to gymnastics like this function wrapping. If we bother about the concrete errors I'd suggest declaring them explicitly in an error enum, at least in new code. so instead of
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. My original aim is to avoid too many error types. Overall, they are only
RCAR attest failed
, because of ananyhow::Error
andRCAR auth failed
, because of ananyhow::Error
. The concrete error inside could be all coverted byanyhow
. I like the concise error type definition style myself