From 10887c3ef65ff51e9fc5c5e99404fea3a9542035 Mon Sep 17 00:00:00 2001 From: ekaterinburg Date: Tue, 30 Sep 2025 14:18:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20CCA=20Attester?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../attestation/attestation-agent/Cargo.toml | 4 + .../attestation-agent/agent/Cargo.toml | 3 +- .../attestation-agent/attester/Cargo.toml | 6 + .../attestation-agent/attester/src/cca/mod.rs | 54 +++++ .../attestation-agent/attester/src/lib.rs | 18 ++ .../attester/src/tsm_report/mod.rs | 199 ++++++++++++++++++ .../attestation-service/Cargo.toml | 2 +- .../verifier/src/rustcca/mod.rs | 13 +- 8 files changed, 291 insertions(+), 8 deletions(-) create mode 100644 service/attestation/attestation-agent/attester/src/cca/mod.rs create mode 100644 service/attestation/attestation-agent/attester/src/tsm_report/mod.rs diff --git a/service/attestation/attestation-agent/Cargo.toml b/service/attestation/attestation-agent/Cargo.toml index c5ef7e9..3a8f143 100644 --- a/service/attestation/attestation-agent/Cargo.toml +++ b/service/attestation/attestation-agent/Cargo.toml @@ -23,5 +23,9 @@ clap = { version = "4.5.7", features = ["derive"] } scc = "2.1" sha2 = "0.10" +strum = { version = "0.25", features = ["derive"] } +tempfile = "3.0" +openssl-sys = { version = "0.9", features = ["vendored"] } + verifier = { path = "../attestation-service/verifier", default-features = false } attestation-types = { path = "../attestation-types" } diff --git a/service/attestation/attestation-agent/agent/Cargo.toml b/service/attestation/attestation-agent/agent/Cargo.toml index 07c1c01..24a327b 100644 --- a/service/attestation/attestation-agent/agent/Cargo.toml +++ b/service/attestation/attestation-agent/agent/Cargo.toml @@ -18,7 +18,8 @@ crate-type = ["lib", "cdylib"] no_as = [] itrustee-attester = ["attester/itrustee-attester"] virtcca-attester = ["attester/virtcca-attester"] -all-attester = ["attester/itrustee-attester", "attester/virtcca-attester"] +cca-attester = ["attester/cca-attester"] +all-attester = ["attester/itrustee-attester", "attester/virtcca-attester", "attester/cca-attester"] itrustee-verifier = ["verifier/itrustee-verifier"] virtcca-verifier = ["verifier/virtcca-verifier"] all-verifier = ["verifier/itrustee-verifier", "verifier/virtcca-verifier"] diff --git a/service/attestation/attestation-agent/attester/Cargo.toml b/service/attestation/attestation-agent/attester/Cargo.toml index f4e5ef3..f953a63 100644 --- a/service/attestation/attestation-agent/attester/Cargo.toml +++ b/service/attestation/attestation-agent/attester/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [features] itrustee-attester = ["base64-url", "rand", "sha2"] virtcca-attester = ["base64-url"] +cca-attester = ["base64-url"] [dependencies] anyhow.workspace = true @@ -17,3 +18,8 @@ async-trait.workspace = true log.workspace = true attestation-types.workspace = true sha2 = { workspace = true, optional = true } + +strum.workspace = true +tempfile.workspace = true +thiserror.workspace = true +openssl-sys.workspace = true \ No newline at end of file diff --git a/service/attestation/attestation-agent/attester/src/cca/mod.rs b/service/attestation/attestation-agent/attester/src/cca/mod.rs new file mode 100644 index 0000000..a61e5a7 --- /dev/null +++ b/service/attestation/attestation-agent/attester/src/cca/mod.rs @@ -0,0 +1,54 @@ +// Copyright (c) 2023-2024 Arm Ltd. +// +// SPDX-License-Identifier: Apache-2.0 +// +use anyhow::{bail, Result, Context}; +use crate::EvidenceRequest; +use super::tsm_report::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default)] +pub struct CcaAttester {} + +const CCA_CHALLENGE_SIZE: usize = 64; + +pub fn detect_platform() -> bool { + #[cfg(target_arch = "aarch64")] + return TsmReportPath::new(TsmReportProvider::Cca).is_ok(); + #[cfg(not(target_arch = "aarch64"))] + return false; +} + +#[derive(Serialize, Deserialize)] +struct CcaEvidence { + /// CCA token + token: Vec, +} + +impl CcaAttester { + pub async fn tee_get_evidence(&self, user_data: EvidenceRequest) -> Result { + let mut challenge = base64_url::decode(&user_data.challenge)?; + + if challenge.len() > CCA_CHALLENGE_SIZE { + bail!("CCA Attester: Challenge size must be {CCA_CHALLENGE_SIZE} bytes or less."); + } + + // 将 challenge 填充到固定大小 + challenge.resize(CCA_CHALLENGE_SIZE, 0); + + // 获取 TSM 报告 + let tsm = TsmReportPath::new(TsmReportProvider::Cca)?; + let token = tsm.attestation_report(TsmReportData::Cca(challenge))?; + + // 构造 CCA 证据 + let evidence = CcaEvidence { token }; + + // 序列化为 JSON 字符串 + let ev = serde_json::to_string(&evidence).context("Serialization of CCA evidence failed")?; + + // 将 JSON 字符串进行 base64_url 编码 + let ev_str = base64_url::encode(&ev); + + Ok(ev_str) + } +} diff --git a/service/attestation/attestation-agent/attester/src/lib.rs b/service/attestation/attestation-agent/attester/src/lib.rs index 25af6cb..0b77285 100644 --- a/service/attestation/attestation-agent/attester/src/lib.rs +++ b/service/attestation/attestation-agent/attester/src/lib.rs @@ -23,6 +23,12 @@ mod itrustee; #[cfg(feature = "virtcca-attester")] pub mod virtcca; +#[cfg(feature = "cca-attester")] +pub mod cca; + +#[cfg(feature = "cca-attester")] +pub mod tsm_report; + // IMA module for handling IMA logs pub mod ima; @@ -71,6 +77,18 @@ impl AttesterAPIs for Attester { let evidence = serde_json::to_vec(&aa_evidence)?; return Ok(evidence); } + #[cfg(feature = "cca-attester")] + if cca::detect_platform() { + let evidence = cca::CcaAttester::default() + .tee_get_evidence(_user_data) + .await?; + let aa_evidence = attestation_types::Evidence { + tee: attestation_types::TeeType::Rustcca, + evidence: evidence, + }; + let evidence = serde_json::to_vec(&aa_evidence)?; + return Ok(evidence); + } bail!("unknown tee platform"); } } diff --git a/service/attestation/attestation-agent/attester/src/tsm_report/mod.rs b/service/attestation/attestation-agent/attester/src/tsm_report/mod.rs new file mode 100644 index 0000000..5b00149 --- /dev/null +++ b/service/attestation/attestation-agent/attester/src/tsm_report/mod.rs @@ -0,0 +1,199 @@ +// Copyright (c) 2024 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use strum::EnumString; +use tempfile::tempdir_in; +use thiserror::Error; + +const TSM_REPORT_PATH: &str = "/sys/kernel/config/tsm/report"; + +#[derive(Error, Debug)] +pub enum TsmReportError { + #[error("Failed to access TSM Report path")] + NoTsmReports, + #[error("Failed to create TSM Report path instance: {0}")] + Open(#[from] std::io::Error), + #[error("Failed to access TSM Report attribute: {0} ({1})")] + Access(&'static str, #[source] std::io::Error), + #[error("Failed to parse TSM Report attribute 'generation': {0}")] + Parse(#[source] std::num::ParseIntError), + #[error("Failed to open TSM Report path: missing provider {0:?} (provider={1:?})")] + MissingProvider(TsmReportProvider, TsmReportProvider), + #[error("Failed to open TSM Report path: unknown provider ({0})")] + UnknownProvider(#[from] strum::ParseError), + #[error("Failed to generate TSM Report: inblob write conflict (generation={0}, expected 1)")] + InblobConflict(u32), + #[error("Failed to generate TSM Report: missing inblob (len=0)")] + InblobLen, +} + +#[derive(PartialEq, Debug, EnumString)] +pub enum TsmReportProvider { + #[strum(serialize = "arm_cca_guest\n")] + Cca, + #[strum(serialize = "tdx_guest\n")] + Tdx, + #[strum(serialize = "sev_guest\n")] + Sev, +} + +pub enum TsmReportData { + Cca(Vec), + Tdx(Vec), + Sev(u8, Vec), +} + +/// TsmReportPath instance represents a unique path on ConfigFS +/// provided by the TSM_REPORT attestation ABI. Currently, each +/// instance is a one-shot attestation request and the path is +/// automatically removed when the instance goes out of scope. +pub struct TsmReportPath { + path: PathBuf, +} + +impl Drop for TsmReportPath { + fn drop(&mut self) { + let _ = std::fs::remove_dir(self.path.as_path()) + .map_err(|e| log::error!("Failed to remove TSM Report directory: {}", e)); + } +} + +impl TsmReportPath { + pub fn new(wanted: TsmReportProvider) -> Result { + if !Path::new(TSM_REPORT_PATH).exists() { + return Err(TsmReportError::NoTsmReports); + } + + let p = tempdir_in(TSM_REPORT_PATH).map_err(TsmReportError::Open)?; + + // Remove the Drop set by tempdir_in() since it errors on ConfigFS + // and leaks the created path. We implement our own Drop that removes the + // path (rmdir way) when TsmReportPath instance goes out of scope. + let path = p.into_path(); + + check_tsm_report_provider(path.as_path(), wanted).inspect_err(|_| { + let _ = std::fs::remove_dir(path.as_path()); + })?; + + Ok(Self { path }) + } + pub fn attestation_report( + &self, + provider_data: TsmReportData, + ) -> Result, TsmReportError> { + let report_path = self.path.as_path(); + + let report_data = match provider_data { + TsmReportData::Cca(inblob) => inblob, + TsmReportData::Tdx(inblob) => inblob, + TsmReportData::Sev(privlevel, inblob) => { + // TODO: untested + std::fs::write(report_path.join("privlevel"), vec![privlevel]) + .map_err(|e| TsmReportError::Access("privlevel", e))?; + inblob + } + }; + + if report_data.is_empty() { + return Err(TsmReportError::InblobLen); + } + + std::fs::write(report_path.join("inblob"), report_data) + .map_err(|e| TsmReportError::Access("inblob", e))?; + + let q = std::fs::read(report_path.join("outblob")) + .map_err(|e| TsmReportError::Access("outblob", e))?; + + check_inblob_write_race(report_path)?; + + Ok(q) + } + pub fn supplemental_data(&self) -> Result, TsmReportError> { + let report_path = self.path.as_path(); + + let aux = std::fs::read(report_path.join("auxblob")) + .map_err(|e| TsmReportError::Access("auxblob", e))?; + + check_inblob_write_race(report_path)?; + + Ok(aux) + } +} + +/// check_inblob_write_race checks that the returned outblob/auxblob +/// matches the quote generation request originally triggered when +/// inblob was written by the TsmReportPath instance. It prevents +/// the race condition that someone else could use the same temporary +/// directory to generate a quote. +fn check_inblob_write_race(report_path: &Path) -> Result<(), TsmReportError> { + let g = std::fs::read_to_string(report_path.join("generation")) + .map_err(|e| TsmReportError::Access("generation", e))?; + + let generation = g + .trim_matches('\n') + .to_string() + .parse::() + .map_err(TsmReportError::Parse)?; + + if generation > 1 { + return Err(TsmReportError::InblobConflict(generation)); + } + + Ok(()) +} + +/// check_tsm_report_provider checks that the TEE is +/// the requested TsmReportProvider. +fn check_tsm_report_provider( + report_path: &Path, + wanted: TsmReportProvider, +) -> Result<(), TsmReportError> { + let report_provider = std::fs::read_to_string(report_path.join("provider")) + .map_err(|e| TsmReportError::Access("provider", e))?; + + match TsmReportProvider::from_str(&report_provider) { + Ok(provider) => { + if provider == wanted { + Ok(()) + } else { + Err(TsmReportError::MissingProvider(wanted, provider)) + } + } + Err(e) => Err(TsmReportError::UnknownProvider(e)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::*; + + #[rstest] + #[case("provider", "tdx_guest\n", false)] + #[case("provider", "sev_guest\n", true)] + #[case("provider", "foo_guest\n", true)] + #[case("generation", "1\n", false)] + #[case("generation", "2\n", true)] + #[case("generation", "parseerror\n", true)] + fn test_tsm_report(#[case] file: &str, #[case] file_data: &str, #[case] expect_error: bool) { + let tsm_dir = tempfile::tempdir().unwrap(); + + std::fs::write(tsm_dir.path().join(file), file_data).unwrap(); + + match file { + "provider" => assert_eq!( + expect_error, + check_tsm_report_provider(tsm_dir.path(), TsmReportProvider::Tdx).is_err() + ), + "generation" => assert_eq!( + expect_error, + check_inblob_write_race(tsm_dir.path()).is_err(), + ), + _ => unimplemented!(), + } + } +} diff --git a/service/attestation/attestation-service/Cargo.toml b/service/attestation/attestation-service/Cargo.toml index ed0ebd2..b327878 100644 --- a/service/attestation/attestation-service/Cargo.toml +++ b/service/attestation/attestation-service/Cargo.toml @@ -36,4 +36,4 @@ scc = "2.1" attestation-types = { path = "../attestation-types" } ear = "0.1.1" -ccatoken = "0.1.0" +ccatoken = { git = "https://github.com/veraison/rust-ccatoken.git" , rev = "dfe9ca2d949c28eb0944fe16769f06a96676dda1" } diff --git a/service/attestation/attestation-service/verifier/src/rustcca/mod.rs b/service/attestation/attestation-service/verifier/src/rustcca/mod.rs index bd2da4c..fe7eab7 100644 --- a/service/attestation/attestation-service/verifier/src/rustcca/mod.rs +++ b/service/attestation/attestation-service/verifier/src/rustcca/mod.rs @@ -28,6 +28,8 @@ use ccatoken::token; use serde_json::value::RawValue; use std::error::Error; +use std::io::Cursor; + const TEST_CPAK: &str = include_str!("../../test_data/cpak.json"); #[derive(Debug, Default)] @@ -43,8 +45,8 @@ impl RustCCAVerifier { // 1. execute golden to get tas, rvs // 2. execute verify fn evalute_wrapper(user_data: &[u8], evidence: &[u8]) -> Result { - let mut in_evidence = - token::Evidence::decode(&evidence.to_vec()).unwrap_or_else(|_| panic!("decode evidence")); + let mut reader = Cursor::new(evidence.to_vec()); + let mut in_evidence = token::Evidence::decode(reader).unwrap_or_else(|_| panic!("decode evidence")); let cpak = map_str_to_cpak(&in_evidence.platform_claims, &TEST_CPAK) .unwrap_or_else(|_| panic!("map cpak")); @@ -80,7 +82,7 @@ fn evalute_wrapper(user_data: &[u8], evidence: &[u8]) -> Result { "challenge" : hex::encode(in_evidence.realm_claims.challenge.clone()), "perso" : hex::encode(in_evidence.realm_claims.perso.clone()), "hash_alg" : hex::encode(in_evidence.realm_claims.hash_alg.clone()), - "rak" : hex::encode(in_evidence.realm_claims.rak.clone()) + "rak" : hex::encode(in_evidence.realm_claims.get_realm_key().expect("get RAK failed")) } }); @@ -200,8 +202,7 @@ mod tests { #[test] fn cca_test() -> Result<(), Box> { - let mut evidence = - token::Evidence::decode(&TEST_CCA_TOKEN.to_vec()).expect("decoding TEST_CCA_TOKEN"); + let mut evidence = token::Evidence::decode(Cursor::new(TEST_CCA_TOKEN.to_vec())).expect("decoding TEST_CCA_TOKEN"); let j = TEST_CPAK; let cpak = map_str_to_cpak(&evidence.platform_claims, &j)?; @@ -232,7 +233,7 @@ mod tests { "challenge" : hex::encode(evidence.realm_claims.challenge.clone()), "perso" : hex::encode(evidence.realm_claims.perso.clone()), "hash_alg" : hex::encode(evidence.realm_claims.hash_alg.clone()), - "rak" : hex::encode(evidence.realm_claims.rak.clone()) + "rak" : hex::encode(evidence.realm_claims.get_realm_key().expect("get RAK failed")) } }); -- Gitee