diff --git a/Cargo.lock b/Cargo.lock index be3e4999de..a9998110b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5709,6 +5709,7 @@ dependencies = [ "serde_with", "serial_test", "sev", + "sha2", "shadow-rs", "strum", "thiserror", diff --git a/attestation-service/verifier/Cargo.toml b/attestation-service/verifier/Cargo.toml index 774b10d51b..8f78b6ff45 100644 --- a/attestation-service/verifier/Cargo.toml +++ b/attestation-service/verifier/Cargo.toml @@ -42,6 +42,7 @@ serde.workspace = true serde_json.workspace = true serde_with = { workspace = true, optional = true } sev = { version = "3.1.1", features = ["openssl", "snp"], optional = true } +sha2.workspace = true tokio = { workspace = true, optional = true, default-features = false } intel-tee-quote-verification-rs = { git = "https://github.com/intel/SGXDataCenterAttestationPrimitives", tag = "DCAP_1.21", optional = true } strum.workspace = true diff --git a/attestation-service/verifier/src/az_tdx_vtpm/mod.rs b/attestation-service/verifier/src/az_tdx_vtpm/mod.rs index 2e85da789c..5ceeb7f90e 100644 --- a/attestation-service/verifier/src/az_tdx_vtpm/mod.rs +++ b/attestation-service/verifier/src/az_tdx_vtpm/mod.rs @@ -63,7 +63,7 @@ impl Verifier for AzTdxVtpm { verify_hcl_var_data(&hcl_report, &td_quote)?; - let mut claim = generate_parsed_claim(td_quote, None)?; + let mut claim = generate_parsed_claim(td_quote, None, None)?; extend_claim_with_tpm_quote(&mut claim, &evidence.tpm_quote)?; Ok(claim) diff --git a/attestation-service/verifier/src/eventlog/hash.rs b/attestation-service/verifier/src/eventlog/hash.rs new file mode 100644 index 0000000000..0b828311b9 --- /dev/null +++ b/attestation-service/verifier/src/eventlog/hash.rs @@ -0,0 +1,22 @@ +// Copyright (c) 2024 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +use strum::{AsRefStr, EnumString}; + +/// Hash algorithms used to calculate eventlog +#[derive(EnumString, AsRefStr, Clone)] +pub enum HashAlgorithm { + #[strum(ascii_case_insensitive)] + #[strum(serialize = "sha256")] + Sha256, + + #[strum(ascii_case_insensitive)] + #[strum(serialize = "sha384")] + Sha384, + + #[strum(ascii_case_insensitive)] + #[strum(serialize = "sha512")] + Sha512, +} diff --git a/attestation-service/verifier/src/eventlog/mod.rs b/attestation-service/verifier/src/eventlog/mod.rs new file mode 100644 index 0000000000..caa9a279a4 --- /dev/null +++ b/attestation-service/verifier/src/eventlog/mod.rs @@ -0,0 +1,180 @@ +// Copyright (c) 2024 Alibaba Cloud +// +// SPDX-License-Identifier: Apache-2.0 +// + +mod hash; + +use std::str::FromStr; + +use anyhow::{anyhow, bail, Context, Result}; +use hash::HashAlgorithm; +use serde_json::{Map, Value}; +use sha2::{Digest, Sha256, Sha384, Sha512}; + +#[derive(Clone)] +pub struct AAEvent { + pub domain: String, + pub operation: String, + pub content: String, +} + +impl FromStr for AAEvent { + type Err = anyhow::Error; + + fn from_str(input: &str) -> Result { + let input_trimed = input.trim_end(); + let sections: Vec<&str> = input_trimed.split(' ').collect(); + if sections.len() != 3 { + bail!("Illegal AA event entry format. Should be ` `"); + } + Ok(Self { + domain: sections[0].into(), + operation: sections[1].into(), + content: sections[2].into(), + }) + } +} + +#[derive(Clone)] +pub struct AAEventlog { + pub hash_algorithm: HashAlgorithm, + pub init_state: Vec, + pub events: Vec, +} + +macro_rules! hash_alg_impl { + ($sha:ident, $self:ident) => {{ + let mut state = $self.init_state.clone(); + + let mut init_event_hasher = $sha::new(); + let init_event = format!( + "INIT {}/{}", + $self.hash_algorithm.as_ref(), + hex::encode(&$self.init_state) + ); + + init_event_hasher.update(init_event.as_bytes()); + let init_event_hash = init_event_hasher.finalize(); + + let mut hasher = $sha::new(); + hasher.update(&state); + + hasher.update(init_event_hash); + state = hasher.finalize().to_vec(); + + $self.events.iter().for_each(|event| { + let mut event_hasher = $sha::new(); + event_hasher.update(event.domain.as_bytes()); + event_hasher.update(b" "); + event_hasher.update(event.operation.as_bytes()); + event_hasher.update(b" "); + event_hasher.update(event.content.as_bytes()); + let event_hash = event_hasher.finalize(); + + let mut hasher = $sha::new(); + hasher.update(&state); + hasher.update(event_hash); + state = hasher.finalize().to_vec(); + }); + + state + }}; +} + +impl FromStr for AAEventlog { + type Err = anyhow::Error; + + fn from_str(input: &str) -> Result { + let event_lines = input.lines().collect::>(); + + let (initline, eventline) = event_lines + .split_first() + .ok_or(anyhow!("at least one line should be included in AAEL"))?; + + // Init line looks like + // INIT sha256/0000000000000000000000000000000000000000000000000000000000000000 + let init_line_items = initline.split_ascii_whitespace().collect::>(); + if init_line_items.len() != 2 { + bail!("Illegal INIT event record."); + } + + if init_line_items[0] != "INIT" { + bail!("INIT event should start with `INIT` key word"); + } + + let (hash_algorithm, init_state) = init_line_items[1].split_once('/').ok_or(anyhow!( + "INIT event should have `/` as content after `INIT`" + ))?; + + let hash_algorithm = hash_algorithm + .try_into() + .context("parse Hash Algorithm in INIT entry")?; + let init_state = hex::decode(init_state).context("parse init state in INIT entry")?; + + let events = eventline + .iter() + .map(|line| AAEvent::from_str(line)) + .collect::>>()?; + + Ok(Self { + events, + hash_algorithm, + init_state, + }) + } +} + +impl AAEventlog { + /// Check the integrity of the AAEL, and gets a digest. The digest should be the same + /// as the input `rtmr`, or the integrity check will fail. + pub fn integrity_check(&self, rtmr: &[u8]) -> Result<()> { + let result = match self.hash_algorithm { + HashAlgorithm::Sha256 => hash_alg_impl!(Sha256, self), + HashAlgorithm::Sha384 => hash_alg_impl!(Sha384, self), + HashAlgorithm::Sha512 => hash_alg_impl!(Sha512, self), + }; + + if rtmr != result { + bail!( + "AA eventlog does not pass check. AAEL value : {}, Quote value {}", + hex::encode(result), + hex::encode(rtmr) + ); + } + + Ok(()) + } + + pub fn to_parsed_claims(&self) -> Map { + let mut aael = Map::new(); + for eventlog in &self.events { + let key = format!("\"{}-{}\"", eventlog.domain, eventlog.operation); + aael.insert(key, serde_json::Value::String(eventlog.content.clone())); + } + + aael + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use rstest::rstest; + + #[rstest] + #[case("./test_data/aael/AAEL_data_1", b"71563a23b430b8637970b866169052815ef9434056516dc9f78c1b3bfb745cee18a2ca92aa53c8122be5cbe59a100764")] + #[case("./test_data/aael/AAEL_data_2", b"31fa17881137923029b1da5b368e92d8b22b14bbb4deaa360da61fce7aa530bd2f4c59ac7bd27021ef64104ff4dd04f9")] + #[case("./test_data/aael/AAEL_data_3", b"0de62b45b29775495d278c85ad63ff45e59406e509506b26c545a5419316e1c4bd2b00a4e803051fa98b550767e13f06")] + fn aael_integrity_check(#[case] aael_path: &str, #[case] sum: &[u8]) { + use std::str::FromStr; + + use super::AAEventlog; + + let aael_bin = fs::read_to_string(aael_path).unwrap(); + let aael = AAEventlog::from_str(&aael_bin).unwrap(); + let sum = hex::decode(sum).unwrap(); + aael.integrity_check(&sum).unwrap(); + } +} diff --git a/attestation-service/verifier/src/lib.rs b/attestation-service/verifier/src/lib.rs index f0e2e7a289..40c09d345a 100644 --- a/attestation-service/verifier/src/lib.rs +++ b/attestation-service/verifier/src/lib.rs @@ -7,6 +7,8 @@ use log::debug; pub mod sample; +pub mod eventlog; + #[cfg(feature = "az-snp-vtpm-verifier")] pub mod az_snp_vtpm; diff --git a/attestation-service/verifier/src/tdx/claims.rs b/attestation-service/verifier/src/tdx/claims.rs index e24171ffbf..168db155dd 100644 --- a/attestation-service/verifier/src/tdx/claims.rs +++ b/attestation-service/verifier/src/tdx/claims.rs @@ -50,7 +50,7 @@ use byteorder::{LittleEndian, ReadBytesExt}; use log::{debug, warn}; use serde_json::{Map, Value}; -use crate::{tdx::quote::QuoteV5Body, TeeEvidenceParsedClaim}; +use crate::{eventlog::AAEventlog, tdx::quote::QuoteV5Body, TeeEvidenceParsedClaim}; use super::{ eventlog::{CcEventLog, MeasuredEntity}, @@ -72,6 +72,7 @@ macro_rules! parse_claim { pub fn generate_parsed_claim( quote: Quote, cc_eventlog: Option, + aa_eventlog: Option, ) -> Result { let mut quote_map = Map::new(); let mut quote_body = Map::new(); @@ -172,6 +173,13 @@ pub fn generate_parsed_claim( } let mut claims = Map::new(); + + // Claims from AA eventlog + if let Some(aael) = aa_eventlog { + let aael_map = aael.to_parsed_claims(); + parse_claim!(claims, "aael", aael_map); + } + parse_claim!(claims, "quote", quote_map); parse_claim!(claims, "ccel", ccel_map); @@ -329,7 +337,7 @@ mod tests { let ccel_bin = std::fs::read("./test_data/CCEL_data").expect("read ccel failed"); let quote = parse_tdx_quote("e_bin).expect("parse quote"); let ccel = CcEventLog::try_from(ccel_bin).expect("parse ccel"); - let claims = generate_parsed_claim(quote, Some(ccel)).expect("parse claim failed"); + let claims = generate_parsed_claim(quote, Some(ccel), None).expect("parse claim failed"); let expected = json!({ "ccel": { "kernel": "5b7aa6572f649714ff00b6a2b9170516a068fd1a0ba72aa8de27574131d454e6396d3bfa1727d9baf421618a942977fa", diff --git a/attestation-service/verifier/src/tdx/eventlog.rs b/attestation-service/verifier/src/tdx/eventlog.rs index fb9fdca333..3278c9e71a 100644 --- a/attestation-service/verifier/src/tdx/eventlog.rs +++ b/attestation-service/verifier/src/tdx/eventlog.rs @@ -46,11 +46,8 @@ impl CcEventLog { if rtmr_from_quote.rtmr0 != rtmr_eventlog.rtmr0 || rtmr_from_quote.rtmr1 != rtmr_eventlog.rtmr1 || rtmr_from_quote.rtmr2 != rtmr_eventlog.rtmr2 - || rtmr_from_quote.rtmr3 != rtmr_eventlog.rtmr3 { - return Err(anyhow!( - "RTMR values from TD quote is not equal with the values from EventLog\n" - )); + bail!("RTMR 0, 1, 2 values from TD quote is not equal with the values from EventLog"); } Ok(()) diff --git a/attestation-service/verifier/src/tdx/mod.rs b/attestation-service/verifier/src/tdx/mod.rs index d4a9f1c384..6878f214a6 100644 --- a/attestation-service/verifier/src/tdx/mod.rs +++ b/attestation-service/verifier/src/tdx/mod.rs @@ -1,7 +1,9 @@ +use std::str::FromStr; + use anyhow::anyhow; use log::{debug, error, info, warn}; -use crate::tdx::claims::generate_parsed_claim; +use crate::{eventlog::AAEventlog, tdx::claims::generate_parsed_claim}; use super::*; use async_trait::async_trait; @@ -21,6 +23,8 @@ struct TdxEvidence { cc_eventlog: Option, // Base64 encoded TD quote. quote: String, + // Eventlog of Attestation Agent + aa_eventlog: Option, } #[derive(Debug, Default)] @@ -105,15 +109,32 @@ async fn verify_evidence( } } + // Verify Integrity of AA eventlog + let aael = match &evidence.aa_eventlog { + Some(el) => { + let aael = + AAEventlog::from_str(el).context("failed to parse AA Eventlog from evidence")?; + // We assume we always use PCR 17, rtmr 3 for the application side events. + + aael.integrity_check(quote.rtmr_3())?; + info!("CCEL integrity check succeeded."); + Some(aael) + } + None => { + warn!("No AA Eventlog included inside the TDX evidence."); + None + } + }; + // Return Evidence parsed claim - generate_parsed_claim(quote, ccel_option) + generate_parsed_claim(quote, ccel_option, aael) } #[cfg(test)] mod tests { use super::*; - use std::fs; + use std::{fs, str::FromStr}; #[test] fn test_generate_parsed_claim() { @@ -122,7 +143,7 @@ mod tests { let quote_bin = fs::read("./test_data/tdx_quote_4.dat").unwrap(); let quote = parse_tdx_quote("e_bin).unwrap(); - let parsed_claim = generate_parsed_claim(quote, Some(ccel)); + let parsed_claim = generate_parsed_claim(quote, Some(ccel), None); assert!(parsed_claim.is_ok()); let _ = fs::write( @@ -130,4 +151,13 @@ mod tests { format!("{:?}", parsed_claim.unwrap()), ); } + + #[test] + fn test_aael_binding() { + let aael_bin = fs::read_to_string("./test_data/aael/AAEL_data_1").unwrap(); + let aael = AAEventlog::from_str(&aael_bin).unwrap(); + let quote_bin = fs::read("./test_data/aael/AAEL_quote_tdx").unwrap(); + let quote = parse_tdx_quote("e_bin).unwrap(); + aael.integrity_check(quote.rtmr_3()).unwrap(); + } } diff --git a/attestation-service/verifier/test_data/aael/AAEL_data_1 b/attestation-service/verifier/test_data/aael/AAEL_data_1 new file mode 100644 index 0000000000..f0fca95a89 --- /dev/null +++ b/attestation-service/verifier/test_data/aael/AAEL_data_1 @@ -0,0 +1,2 @@ +INIT sha384/000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +image-rs CreateContainer docker.io/library/alpine \ No newline at end of file diff --git a/attestation-service/verifier/test_data/aael/AAEL_data_2 b/attestation-service/verifier/test_data/aael/AAEL_data_2 new file mode 100644 index 0000000000..1f5e6b46fc --- /dev/null +++ b/attestation-service/verifier/test_data/aael/AAEL_data_2 @@ -0,0 +1,2 @@ +INIT sha384/000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +github.com/confidential-containers CreateContainer docker.io/library/alpine \ No newline at end of file diff --git a/attestation-service/verifier/test_data/aael/AAEL_data_3 b/attestation-service/verifier/test_data/aael/AAEL_data_3 new file mode 100644 index 0000000000..29d1455166 --- /dev/null +++ b/attestation-service/verifier/test_data/aael/AAEL_data_3 @@ -0,0 +1,3 @@ +INIT sha384/000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +github.com/confidential-containers CreateContainer docker.io/library/alpine +github.com/confidential-containers CreateContainer docker.io/library/busybox \ No newline at end of file diff --git a/attestation-service/verifier/test_data/aael/AAEL_quote_tdx b/attestation-service/verifier/test_data/aael/AAEL_quote_tdx new file mode 100644 index 0000000000..47c500f165 Binary files /dev/null and b/attestation-service/verifier/test_data/aael/AAEL_quote_tdx differ