Skip to content
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
wants to merge 13 commits into
base: main
Choose a base branch
from
362 changes: 362 additions & 0 deletions kbs/src/attestation/backend.rs
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(
Copy link
Contributor

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() in myfunc() or if they need to be separated can we put it into a module?

Copy link
Member Author

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] upon pub async fn attest(...)?

Copy link
Contributor

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)

Copy link
Member Author

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 to attest, we need a lot of map_err from anyhow::Error to Error

Copy link
Contributor

@mkulke mkulke Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if Error is using thiserror macros, we should be able to use a Misc(#[from] anyhow::Error) entry in the error enum, or it even works automatically, I don't remember

Copy link
Member Author

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 ALL anyhow:Error to Error with ?. In this case we want to do some classfication upon the different anyhow::Error due to Auth and Attest of RCAR

Copy link
Contributor

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

request.cookie(KBS_SESSION_ID).context("cookie not found")?;
#[error("cookie not found")]
CookieNotFound,
...
request.cookie(KBS_SESSION_ID).ok_or(Error::CookieNotFound)?;

Copy link
Member Author

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 an anyhow::Error and RCAR auth failed, because of an anyhow::Error. The concrete error inside could be all coverted by anyhow. I like the concise error type definition style myself

&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);
}
}
}
3 changes: 2 additions & 1 deletion kbs/src/attestation/coco/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
// 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};
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<AttestationService>,
}
Expand Down
Loading