diff --git a/Cargo.lock b/Cargo.lock index e7485c1e..754d36fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1418,6 +1418,33 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.3.0" @@ -2243,6 +2270,7 @@ version = "0.5.8" dependencies = [ "anyhow", "cc-eventlog", + "ciborium", "dcap-qvl", "dstack-types", "errify", @@ -3420,6 +3448,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash_hasher" version = "2.0.4" diff --git a/dstack-attest/Cargo.toml b/dstack-attest/Cargo.toml index 7ee9783a..90a1c733 100644 --- a/dstack-attest/Cargo.toml +++ b/dstack-attest/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true [dependencies] anyhow.workspace = true cc-eventlog.workspace = true +ciborium.workspace = true dcap-qvl.workspace = true dstack-types.workspace = true ez-hash.workspace = true diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index ed5d9623..bdcee354 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -22,11 +22,13 @@ use dstack_types::SysConfig; use dstack_types::{Platform, VmConfig}; use ez_hash::{sha256, Hasher, Sha384}; use or_panic::ResultOrPanic; -use scale::{Decode, Encode}; +use scale::{Decode, Encode, Error as ScaleError, Input, Output}; use serde::{Deserialize, Serialize}; use serde_human_bytes as hex_bytes; use sha2::Digest as _; +pub use crate::v1::{Attestation as AttestationV1, PlatformEvidence, StackEvidence}; + const DSTACK_TDX: &str = "dstack-tdx"; const DSTACK_GCP_TDX: &str = "dstack-gcp-tdx"; const DSTACK_NITRO_ENCLAVE: &str = "dstack-nitro-enclave"; @@ -50,6 +52,91 @@ fn read_vm_config() -> Result { Ok(sys_config.vm_config) } +fn is_cbor_map_prefix(byte: u8) -> bool { + matches!(byte, 0xa0..=0xbb | 0xbf) +} + +impl From for AttestationV1 { + fn from(attestation: Attestation) -> Self { + let Attestation { + quote, + runtime_events, + report_data, + config, + report: _, + } = attestation; + + let platform = platform_from_legacy_quote(quote); + let stack = StackEvidence::Dstack { + report_data: report_data.to_vec(), + runtime_events, + config, + }; + Self::new(platform, stack) + } +} + +fn platform_from_legacy_quote(quote: AttestationQuote) -> PlatformEvidence { + match quote { + AttestationQuote::DstackTdx(TdxQuote { quote, event_log }) => { + PlatformEvidence::Tdx { quote, event_log } + } + AttestationQuote::DstackGcpTdx => PlatformEvidence::GcpTdx, + AttestationQuote::DstackNitroEnclave => PlatformEvidence::NitroEnclave, + } +} + +fn platform_into_legacy_quote(platform: PlatformEvidence) -> AttestationQuote { + match platform { + PlatformEvidence::Tdx { quote, event_log } => { + AttestationQuote::DstackTdx(TdxQuote { quote, event_log }) + } + PlatformEvidence::GcpTdx => AttestationQuote::DstackGcpTdx, + PlatformEvidence::NitroEnclave => AttestationQuote::DstackNitroEnclave, + } +} + +fn platform_attestation_mode(platform: &PlatformEvidence) -> AttestationMode { + match platform { + PlatformEvidence::Tdx { .. } => AttestationMode::DstackTdx, + PlatformEvidence::GcpTdx => AttestationMode::DstackGcpTdx, + PlatformEvidence::NitroEnclave => AttestationMode::DstackNitroEnclave, + } +} + +fn replay_runtime_events( + runtime_events: &[RuntimeEvent], + to_event: Option<&str>, +) -> H::Output { + cc_eventlog::replay_events::(runtime_events, to_event) +} + +fn find_event(runtime_events: &[RuntimeEvent], name: &str) -> Result { + for event in runtime_events { + if event.event == "system-ready" { + break; + } + if event.event == name { + return Ok(event.clone()); + } + } + Err(anyhow!("event {name} not found")) +} + +fn find_event_payload(runtime_events: &[RuntimeEvent], event: &str) -> Result> { + find_event(runtime_events, event).map(|event| event.payload) +} + +fn decode_vm_config_with_fallback(config: &str, fallback_config: &str) -> Result { + let config = if config.is_empty() { + fallback_config + } else { + config + }; + let config = if config.is_empty() { "{}" } else { config }; + serde_json::from_str(config).context("Failed to parse vm config") +} + /// Attestation mode #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)] pub enum AttestationMode { @@ -237,53 +324,288 @@ pub struct NsmQuote { pub document: Vec, } -/// Represents a versioned attestation #[derive(Clone, Encode, Decode)] +enum LegacyVersionedAttestation { + V0 { attestation: Attestation }, +} + +/// Represents a versioned attestation. +#[derive(Clone)] pub enum VersionedAttestation { - /// Version 0 + /// Legacy SCALE-encoded attestation. V0 { /// The attestation report attestation: Attestation, }, + /// CBOR-encoded attestation schema. + V1 { + /// The version 1 attestation. + attestation: AttestationV1, + }, +} + +impl Encode for VersionedAttestation { + fn size_hint(&self) -> usize { + self.to_bytes().len() + } + + fn encode_to(&self, dest: &mut T) { + dest.write(&self.to_bytes()); + } +} + +impl Decode for VersionedAttestation { + fn decode(input: &mut I) -> Result { + let Some(remaining_len) = input.remaining_len()? else { + return Err(ScaleError::from( + "VersionedAttestation requires a bounded input to decode", + )); + }; + let mut bytes = vec![0u8; remaining_len]; + input.read(&mut bytes)?; + Self::from_bytes(&bytes).map_err(|err| { + ScaleError::from(std::io::Error::new( + std::io::ErrorKind::InvalidData, + err.to_string(), + )) + }) + } } impl VersionedAttestation { - /// Decode VerifiedAttestation from scale encoded bytes - pub fn from_scale(scale: &[u8]) -> Result { - Self::decode(&mut &scale[..]).context("Failed to decode VersionedAttestation") + /// Decode versioned attestation bytes. + pub fn from_bytes(bytes: &[u8]) -> Result { + let Some(first) = bytes.first().copied() else { + bail!("Empty attestation bytes"); + }; + if first == 0x00 { + let legacy = LegacyVersionedAttestation::decode(&mut &bytes[..]) + .context("Failed to decode legacy VersionedAttestation")?; + return match legacy { + LegacyVersionedAttestation::V0 { attestation } => Ok(Self::V0 { attestation }), + }; + } + if is_cbor_map_prefix(first) { + let attestation = AttestationV1::from_cbor(bytes)?; + return Ok(Self::V1 { attestation }); + } + if first == 0x01 && bytes.get(1).is_some_and(|byte| is_cbor_map_prefix(*byte)) { + let attestation = AttestationV1::from_cbor(&bytes[1..])?; + return Ok(Self::V1 { attestation }); + } + bail!("Unknown attestation wire format"); + } + + /// Encode versioned attestation bytes. + pub fn to_bytes(&self) -> Vec { + match self { + Self::V0 { attestation } => LegacyVersionedAttestation::V0 { + attestation: attestation.clone(), + } + .encode(), + Self::V1 { attestation } => attestation + .to_cbor() + .or_panic("attestation schema should encode as CBOR"), + } + } + + #[doc(hidden)] + pub fn from_scale(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) } - /// Encode to scale encoded bytes + #[doc(hidden)] pub fn to_scale(&self) -> Vec { - self.encode() + self.to_bytes() } - /// Turn into latest version of attestation - pub fn into_inner(self) -> Attestation { + /// Project any version into the V1 attestation schema. + pub fn into_v1(self) -> AttestationV1 { match self { - Self::V0 { attestation } => attestation, + Self::V0 { attestation } => attestation.into_v1(), + Self::V1 { attestation } => attestation, } } - /// Get app info - pub fn decode_app_info(&self, boottime_mr: bool) -> Result { + /// Strip data for certificate embedding (e.g. keep RTMR3 event logs only). + pub fn into_stripped(self) -> Self { match self { - Self::V0 { attestation } => attestation.decode_app_info(boottime_mr), + Self::V0 { mut attestation } => { + if let Some(tdx_quote) = attestation.tdx_quote_mut() { + tdx_quote.event_log = tdx_quote + .event_log + .iter() + .filter(|e| e.imr == 3) + .map(|e| e.stripped()) + .collect(); + } + Self::V0 { attestation } + } + Self::V1 { attestation } => Self::V1 { + attestation: attestation.into_stripped(), + }, } } +} - /// Strip data for certificate embedding (e.g. keep RTMR3 event logs only). - pub fn into_stripped(mut self) -> Self { - let VersionedAttestation::V0 { attestation } = &mut self; - if let Some(tdx_quote) = attestation.tdx_quote_mut() { - tdx_quote.event_log = tdx_quote - .event_log - .iter() - .filter(|e| e.imr == 3) - .map(|e| e.stripped()) - .collect(); +/// TDX-specific helpers for attestation schemas that carry TDX platform evidence. +pub trait TdxAttestationExt { + /// Returns the raw TDX quote bytes if the attestation is backed by TDX. + fn tdx_quote_bytes(&self) -> Option>; + + /// Returns the parsed TDX event log if the attestation is backed by TDX. + fn tdx_event_log(&self) -> Option<&[TdxEvent]>; + + /// Returns the TDX event log serialized as JSON. + fn tdx_event_log_string(&self) -> Option { + self.tdx_event_log() + .map(|event_log| serde_json::to_string(event_log).unwrap_or_default()) + } + + /// Returns the parsed TD10 report from the embedded TDX quote. + fn td10_report(&self) -> Option; +} + +impl TdxAttestationExt for AttestationV1 { + fn tdx_quote_bytes(&self) -> Option> { + self.platform.tdx_quote().map(|quote| quote.to_vec()) + } + + fn tdx_event_log(&self) -> Option<&[TdxEvent]> { + self.platform.tdx_event_log() + } + + fn td10_report(&self) -> Option { + self.platform + .tdx_quote() + .and_then(|quote| Quote::parse(quote).ok()) + .and_then(|quote| quote.report.as_td10().cloned()) + } +} + +impl AttestationV1 { + /// Decode the VM config from the external or embedded config. + pub fn decode_vm_config<'a>(&'a self, config: &'a str) -> Result { + decode_vm_config_with_fallback(config, self.stack.config()) + } + + /// Decode the app info from the event log. + pub fn decode_app_info(&self, boottime_mr: bool) -> Result { + self.decode_app_info_ex(boottime_mr, "") + } + + /// Decode the app info from the event log with an optional external vm_config. + #[errify::errify("decode app info")] + pub fn decode_app_info_ex(&self, boottime_mr: bool, vm_config: &str) -> Result { + let runtime_events = self.stack.runtime_events(); + let key_provider_info = if boottime_mr { + vec![] + } else { + find_event_payload(runtime_events, "key-provider").unwrap_or_default() + }; + let mr_key_provider = if key_provider_info.is_empty() { + [0u8; 32] + } else { + sha256(&key_provider_info) + }; + let os_image_hash = self + .decode_vm_config(vm_config) + .context("Failed to decode os image hash")? + .os_image_hash; + let mrs = match &self.platform { + PlatformEvidence::Tdx { quote, .. } => { + decode_mr_tdx_from_quote(boottime_mr, &mr_key_provider, quote, runtime_events)? + } + PlatformEvidence::GcpTdx | PlatformEvidence::NitroEnclave => { + bail!("Unsupported attestation quote"); + } + }; + let compose_hash = if platform_attestation_mode(&self.platform).is_composable() { + find_event_payload(runtime_events, "compose-hash").unwrap_or_default() + } else { + os_image_hash.clone() + }; + Ok(AppInfo { + app_id: find_event_payload(runtime_events, "app-id").unwrap_or_default(), + instance_id: find_event_payload(runtime_events, "instance-id").unwrap_or_default(), + device_id: sha256(Vec::::new()).to_vec(), + mr_system: mrs.mr_system, + mr_aggregated: mrs.mr_aggregated, + key_provider_info, + os_image_hash, + compose_hash, + }) + } + + /// Verify the quote with optional custom time (testing hook). + pub async fn verify_with_time( + self, + pccs_url: Option<&str>, + _now: Option, + ) -> Result { + let AttestationV1 { + version: _, + platform, + stack, + } = self; + let (report_data, runtime_events, config) = match stack { + StackEvidence::Dstack { + report_data, + runtime_events, + config, + } + | StackEvidence::DstackPod { + report_data, + runtime_events, + config, + .. + } => ( + report_data + .as_slice() + .try_into() + .map_err(|_| anyhow!("stack.report_data must be 64 bytes"))?, + runtime_events, + config, + ), + }; + let report = match &platform { + PlatformEvidence::Tdx { quote, .. } => DstackVerifiedReport::DstackTdx( + verify_tdx_quote_with_events(pccs_url, quote, &runtime_events, &report_data) + .await?, + ), + PlatformEvidence::GcpTdx | PlatformEvidence::NitroEnclave => { + bail!( + "Unsupported attestation mode: {:?}", + platform_attestation_mode(&platform) + ); + } + }; + + Ok(VerifiedAttestation { + quote: platform_into_legacy_quote(platform), + runtime_events, + report_data, + config, + report, + }) + } + + /// Verify the quote against a RA-TLS public key. + pub async fn verify_with_ra_pubkey( + self, + ra_pubkey_der: &[u8], + pccs_url: Option<&str>, + ) -> Result { + let expected_report_data = QuoteContentType::RaTlsCert.to_report_data(ra_pubkey_der); + if self.report_data()? != expected_report_data { + bail!("report data mismatch"); } - self + self.verify(pccs_url).await + } + + /// Verify the quote. + pub async fn verify(self, pccs_url: Option<&str>) -> Result { + self.verify_with_time(pccs_url, None).await } } @@ -324,6 +646,10 @@ pub struct Attestation { } impl Attestation { + pub fn report_data_payload(&self) -> Option<&str> { + None + } + pub fn tdx_quote_mut(&mut self) -> Option<&mut TdxQuote> { match &mut self.quote { AttestationQuote::DstackTdx(quote) => Some(quote), @@ -392,6 +718,85 @@ struct Mrs { mr_aggregated: [u8; 32], } +fn decode_mr_tdx_from_quote( + boottime_mr: bool, + mr_key_provider: &[u8], + quote: &[u8], + runtime_events: &[RuntimeEvent], +) -> Result { + let quote = Quote::parse(quote).context("Failed to parse quote")?; + let rtmr3 = + replay_runtime_events::(runtime_events, boottime_mr.then_some("boot-mr-done")); + let td_report = quote.report.as_td10().context("TDX report not found")?; + let mr_system = sha256([ + &td_report.mr_td[..], + &td_report.rt_mr0, + &td_report.rt_mr1, + &td_report.rt_mr2, + mr_key_provider, + ]); + let mr_aggregated = { + let mut hasher = sha2::Sha256::new(); + for d in [ + &td_report.mr_td, + &td_report.rt_mr0, + &td_report.rt_mr1, + &td_report.rt_mr2, + &rtmr3, + ] { + hasher.update(d); + } + if td_report.mr_config_id != [0u8; 48] + || td_report.mr_owner != [0u8; 48] + || td_report.mr_owner_config != [0u8; 48] + { + hasher.update(td_report.mr_config_id); + hasher.update(td_report.mr_owner); + hasher.update(td_report.mr_owner_config); + } + hasher.finalize().into() + }; + Ok(Mrs { + mr_system, + mr_aggregated, + }) +} + +async fn verify_tdx_quote_with_events( + pccs_url: Option<&str>, + quote: &[u8], + runtime_events: &[RuntimeEvent], + report_data: &[u8; 64], +) -> Result { + let mut pccs_url = Cow::Borrowed(pccs_url.unwrap_or_default()); + if pccs_url.is_empty() { + pccs_url = match std::env::var("PCCS_URL") { + Ok(url) => Cow::Owned(url), + Err(_) => Cow::Borrowed(""), + }; + } + let tdx_report = + dcap_qvl::collateral::get_collateral_and_verify(quote, Some(pccs_url.as_ref())) + .await + .context("Failed to get collateral")?; + validate_tcb(&tdx_report)?; + + let td_report = tdx_report.report.as_td10().context("no td report")?; + let replayed_rtmr = replay_runtime_events::(runtime_events, None); + if replayed_rtmr != td_report.rt_mr3 { + bail!( + "RTMR3 mismatch, quoted: {}, replayed: {}", + hex::encode(td_report.rt_mr3), + hex::encode(replayed_rtmr) + ); + } + + if td_report.report_data != report_data[..] { + bail!("tdx report_data mismatch"); + } + Ok(tdx_report) +} + impl Attestation { fn decode_mr_tdx( &self, @@ -639,6 +1044,10 @@ impl Attestation { } impl Attestation { + pub fn into_v1(self) -> AttestationV1 { + self.into() + } + /// Verify the quote with optional custom time (testing hook) pub async fn verify_with_time( self, @@ -784,6 +1193,66 @@ pub struct AppInfo { mod tests { use super::*; + fn patch_v1_report_data(attestation: AttestationV1, report_data: [u8; 64]) -> AttestationV1 { + let AttestationV1 { + version, + platform, + stack, + } = attestation; + let platform = match platform { + PlatformEvidence::Tdx { + mut quote, + event_log, + } => { + if quote.len() >= TDX_QUOTE_REPORT_DATA_RANGE.end { + quote[TDX_QUOTE_REPORT_DATA_RANGE].copy_from_slice(&report_data); + } + PlatformEvidence::Tdx { quote, event_log } + } + other => other, + }; + let stack = match stack { + StackEvidence::Dstack { + runtime_events, + config, + .. + } => StackEvidence::Dstack { + report_data: report_data.to_vec(), + runtime_events, + config, + }, + StackEvidence::DstackPod { + runtime_events, + config, + report_data_payload, + .. + } => StackEvidence::DstackPod { + report_data: report_data.to_vec(), + runtime_events, + config, + report_data_payload, + }, + }; + AttestationV1 { + version, + platform, + stack, + } + } + + fn dummy_tdx_attestation(report_data: [u8; 64]) -> Attestation { + Attestation { + quote: AttestationQuote::DstackTdx(TdxQuote { + quote: vec![0u8; TDX_QUOTE_REPORT_DATA_RANGE.end], + event_log: Vec::new(), + }), + runtime_events: Vec::new(), + report_data, + config: "{}".into(), + report: (), + } + } + #[test] fn test_to_report_data_with_hash() { let content_type = QuoteContentType::AppData; @@ -831,4 +1300,63 @@ mod tests { .to_report_data_with_hash(content, "invalid") .is_err()); } + + #[test] + fn v1_roundtrip_preserves_payload_in_stack() { + let report_data = [42u8; 64]; + let payload = r#"{"pod_uid":"abc","workload_id":"default/app"}"#.to_string(); + let attestation = dummy_tdx_attestation(report_data) + .into_v1() + .into_dstack_pod(payload.clone()); + let encoded = VersionedAttestation::V1 { attestation }.to_bytes(); + assert!(matches!(encoded.first(), Some(0xa0..=0xbf))); + let decoded = VersionedAttestation::from_bytes(&encoded) + .expect("decode attestation") + .into_v1(); + assert_eq!(decoded.report_data_payload(), Some(payload.as_str())); + assert_eq!(decoded.report_data().unwrap(), report_data); + let attestation = decoded; + assert!(matches!(attestation.platform, PlatformEvidence::Tdx { .. })); + assert!(matches!( + attestation.stack, + StackEvidence::DstackPod { + report_data_payload, .. + } if report_data_payload == payload + )); + } + + #[test] + fn patching_v1_report_data_preserves_payload_in_stack() { + let original = dummy_tdx_attestation([1u8; 64]) + .into_v1() + .into_dstack_pod("payload".into()); + let patched = patch_v1_report_data(original, [9u8; 64]); + assert_eq!(patched.report_data_payload(), Some("payload")); + assert_eq!(patched.report_data().unwrap(), [9u8; 64]); + } + + #[test] + fn legacy_v0_upgrade_uses_dstack_stack() { + let upgraded = dummy_tdx_attestation([3u8; 64]).into_v1(); + assert!(matches!(upgraded.platform, PlatformEvidence::Tdx { .. })); + assert!(matches!(upgraded.stack, StackEvidence::Dstack { .. })); + } + + #[test] + fn versioned_v0_projects_to_v1() { + let projected = dummy_tdx_attestation([5u8; 64]).into_versioned().into_v1(); + assert!(matches!(projected.platform, PlatformEvidence::Tdx { .. })); + match projected.stack { + StackEvidence::Dstack { + report_data, + runtime_events, + config, + } => { + assert_eq!(report_data, vec![5u8; 64]); + assert!(runtime_events.is_empty()); + assert_eq!(config, "{}"); + } + _ => panic!("expected dstack stack"), + } + } } diff --git a/dstack-attest/src/lib.rs b/dstack-attest/src/lib.rs index 179dd861..1f7bc814 100644 --- a/dstack-attest/src/lib.rs +++ b/dstack-attest/src/lib.rs @@ -11,6 +11,7 @@ pub use tdx_attest as tdx; use crate::attestation::AttestationMode; pub mod attestation; +mod v1; /// Emit a runtime event that extends RTMR3 and logs the event. pub fn emit_runtime_event(event: &str, payload: &[u8]) -> anyhow::Result<()> { diff --git a/dstack-attest/src/v1.rs b/dstack-attest/src/v1.rs new file mode 100644 index 00000000..effc5436 --- /dev/null +++ b/dstack-attest/src/v1.rs @@ -0,0 +1,255 @@ +// SPDX-FileCopyrightText: © 2024-2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{anyhow, bail, Context, Result}; +use cc_eventlog::{RuntimeEvent, TdxEvent}; +use serde::{Deserialize, Serialize}; + +pub const ATTESTATION_VERSION: u64 = 1; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "kind", content = "data")] +pub enum PlatformEvidence { + #[serde(rename = "tdx")] + Tdx { + quote: Vec, + event_log: Vec, + }, + #[serde(rename = "gcp-tdx")] + GcpTdx, + #[serde(rename = "nitro-enclave")] + NitroEnclave, +} + +impl PlatformEvidence { + pub fn tdx_quote(&self) -> Option<&[u8]> { + match self { + Self::Tdx { quote, .. } => Some(quote.as_slice()), + _ => None, + } + } + + pub fn tdx_event_log(&self) -> Option<&[TdxEvent]> { + match self { + Self::Tdx { event_log, .. } => Some(event_log.as_slice()), + _ => None, + } + } + + pub fn into_stripped(self) -> Self { + match self { + Self::Tdx { quote, event_log } => Self::Tdx { + quote, + event_log: event_log + .into_iter() + .filter(|event| event.imr == 3) + .map(|event| event.stripped()) + .collect(), + }, + other => other, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "kind", content = "data")] +pub enum StackEvidence { + #[serde(rename = "dstack")] + Dstack { + report_data: Vec, + runtime_events: Vec, + config: String, + }, + #[serde(rename = "dstack-pod")] + DstackPod { + report_data: Vec, + runtime_events: Vec, + config: String, + report_data_payload: String, + }, +} + +fn decode_report_data(report_data: &[u8]) -> Result<[u8; 64]> { + report_data + .try_into() + .map_err(|_| anyhow!("stack.report_data must be 64 bytes")) +} + +impl StackEvidence { + pub fn report_data(&self) -> Result<[u8; 64]> { + match self { + Self::Dstack { report_data, .. } | Self::DstackPod { report_data, .. } => { + decode_report_data(report_data) + } + } + } + + pub fn runtime_events(&self) -> &[RuntimeEvent] { + match self { + Self::Dstack { runtime_events, .. } | Self::DstackPod { runtime_events, .. } => { + runtime_events.as_slice() + } + } + } + + pub fn config(&self) -> &str { + match self { + Self::Dstack { config, .. } | Self::DstackPod { config, .. } => config, + } + } + + pub fn report_data_payload(&self) -> Option<&str> { + match self { + Self::Dstack { .. } => None, + Self::DstackPod { + report_data_payload, + .. + } => Some(report_data_payload.as_str()), + } + } + + pub fn into_dstack_pod(self, report_data_payload: String) -> Self { + match self { + Self::Dstack { + report_data, + runtime_events, + config, + } + | Self::DstackPod { + report_data, + runtime_events, + config, + .. + } => Self::DstackPod { + report_data, + runtime_events, + config, + report_data_payload, + }, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Attestation { + pub version: u64, + pub platform: PlatformEvidence, + pub stack: StackEvidence, +} + +impl Attestation { + pub fn new(platform: PlatformEvidence, stack: StackEvidence) -> Self { + Self { + version: ATTESTATION_VERSION, + platform, + stack, + } + } + + pub fn to_cbor(&self) -> Result> { + let mut normalized = self.clone(); + normalized.version = ATTESTATION_VERSION; + let mut bytes = Vec::new(); + ciborium::into_writer(&normalized, &mut bytes) + .context("Failed to encode attestation as CBOR")?; + Ok(bytes) + } + + pub fn from_cbor(bytes: &[u8]) -> Result { + let value: Self = + ciborium::from_reader(bytes).context("Failed to decode attestation from CBOR")?; + if value.version != ATTESTATION_VERSION { + bail!( + "Unsupported attestation version: expected {}, got {}", + ATTESTATION_VERSION, + value.version + ); + } + Ok(value) + } + + pub fn report_data(&self) -> Result<[u8; 64]> { + self.stack.report_data() + } + + pub fn report_data_payload(&self) -> Option<&str> { + self.stack.report_data_payload() + } + + pub fn into_stripped(self) -> Self { + Self { + version: self.version, + platform: self.platform.into_stripped(), + stack: self.stack, + } + } + + pub fn into_dstack_pod(self, report_data_payload: String) -> Self { + Self { + version: self.version, + platform: self.platform, + stack: self.stack.into_dstack_pod(report_data_payload), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cbor_roundtrip_preserves_attestation() { + let attestation = Attestation::new( + PlatformEvidence::Tdx { + quote: vec![1u8, 2, 3], + event_log: vec![TdxEvent { + imr: 3, + event_type: 0x08000001, + digest: vec![0xaa, 0xbb, 0xcc], + event: "pod".into(), + event_payload: vec![0xde, 0xad, 0xbe, 0xef], + }], + }, + StackEvidence::DstackPod { + report_data: vec![7u8; 64], + runtime_events: vec![RuntimeEvent { + event: "pod".into(), + payload: vec![0xca, 0xfe, 0xba, 0xbe], + }], + config: "{}".into(), + report_data_payload: "{\"hello\":\"world\"}".into(), + }, + ); + + let encoded = attestation.to_cbor().expect("encode cbor"); + assert!(matches!(encoded.first(), Some(0xa0..=0xbf))); + let decoded = Attestation::from_cbor(&encoded).expect("decode cbor"); + assert_eq!(decoded.version, ATTESTATION_VERSION); + match decoded.platform { + PlatformEvidence::Tdx { quote, event_log } => { + assert_eq!(quote, vec![1u8, 2, 3]); + assert_eq!(event_log.len(), 1); + assert_eq!(event_log[0].event, "pod"); + assert_eq!(event_log[0].event_payload, vec![0xde, 0xad, 0xbe, 0xef]); + } + _ => panic!("expected tdx platform evidence"), + } + match decoded.stack { + StackEvidence::DstackPod { + report_data, + runtime_events, + config, + report_data_payload, + } => { + assert_eq!(report_data, vec![7u8; 64]); + assert_eq!(runtime_events.len(), 1); + assert_eq!(runtime_events[0].event, "pod"); + assert_eq!(runtime_events[0].payload, vec![0xca, 0xfe, 0xba, 0xbe]); + assert_eq!(config, "{}"); + assert_eq!(report_data_payload, "{\"hello\":\"world\"}"); + } + _ => panic!("expected dstack-pod stack evidence"), + } + } +} diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index a3d6ef7b..05ad9ad6 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -243,6 +243,7 @@ fn cmd_show_mrs() -> Result<()> { let attestation = ra_tls::attestation::Attestation::local().context("Failed to get attestation")?; let app_info = attestation + .into_v1() .decode_app_info(false) .context("Failed to decode app info")?; serde_json::to_writer_pretty(io::stdout(), &app_info).context("Failed to write app info")?; diff --git a/guest-agent-simulator/src/main.rs b/guest-agent-simulator/src/main.rs index 65e0b0b9..704f6c2b 100644 --- a/guest-agent-simulator/src/main.rs +++ b/guest-agent-simulator/src/main.rs @@ -14,7 +14,6 @@ use dstack_guest_agent::{ run_server, AppState, }; use dstack_guest_agent_rpc::{AttestResponse, GetQuoteResponse}; -use ra_rpc::Attestation; use ra_tls::attestation::VersionedAttestation; use serde::Deserialize; use tracing::warn; @@ -66,7 +65,7 @@ fn default_patch_report_data() -> bool { } impl PlatformBackend for SimulatorPlatform { - fn attestation_for_info(&self) -> Result { + fn attestation_for_info(&self) -> Result { Ok(simulator::simulated_info_attestation(&self.attestation)) } @@ -163,7 +162,7 @@ mod tests { let cert_attestation = platform .certificate_attestation(b"test-public-key") .unwrap(); - assert!(cert_attestation.decode_app_info(false).is_ok()); + assert!(cert_attestation.into_v1().decode_app_info(false).is_ok()); let _ = platform.attestation_for_info().unwrap(); } @@ -172,9 +171,10 @@ mod tests { let platform = load_fixture_platform(); let report_data = [0x5a; 64]; let response = platform.attest_response(report_data).unwrap(); - let patched = VersionedAttestation::from_scale(&response.attestation).unwrap(); - let VersionedAttestation::V0 { attestation } = patched; - assert_eq!(attestation.report_data, report_data); + let patched = VersionedAttestation::from_bytes(&response.attestation) + .unwrap() + .into_v1(); + assert_eq!(patched.report_data().unwrap(), report_data); } #[test] @@ -184,13 +184,14 @@ mod tests { .join("../guest-agent/fixtures/attestation.bin"), ) .expect("fixture attestation should load"); - let original = fixture.clone().into_inner().report_data; + let original = fixture.clone().into_v1().report_data().unwrap(); let platform = SimulatorPlatform::new(fixture, false); let report_data = [0x5a; 64]; let response = platform.attest_response(report_data).unwrap(); - let patched = VersionedAttestation::from_scale(&response.attestation).unwrap(); - let VersionedAttestation::V0 { attestation } = patched; - assert_eq!(attestation.report_data, original); - assert_ne!(attestation.report_data, report_data); + let patched = VersionedAttestation::from_bytes(&response.attestation) + .unwrap() + .into_v1(); + assert_eq!(patched.report_data().unwrap(), original); + assert_ne!(patched.report_data().unwrap(), report_data); } } diff --git a/guest-agent-simulator/src/simulator.rs b/guest-agent-simulator/src/simulator.rs index abe1781f..84dda80f 100644 --- a/guest-agent-simulator/src/simulator.rs +++ b/guest-agent-simulator/src/simulator.rs @@ -4,10 +4,12 @@ use std::path::Path; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use dstack_guest_agent_rpc::{AttestResponse, GetQuoteResponse}; -use ra_rpc::Attestation; -use ra_tls::attestation::{QuoteContentType, VersionedAttestation, TDX_QUOTE_REPORT_DATA_RANGE}; +use ra_tls::attestation::{ + AttestationV1, PlatformEvidence, QuoteContentType, StackEvidence, TdxAttestationExt, + VersionedAttestation, TDX_QUOTE_REPORT_DATA_RANGE, +}; use std::fs; use tracing::warn; @@ -19,7 +21,7 @@ pub fn load_versioned_attestation(path: impl AsRef) -> Result Result { - let VersionedAttestation::V0 { attestation } = - maybe_patch_report_data(attestation, report_data, patch_report_data, "quote"); - let mut attestation = attestation; - let Some(quote) = attestation.tdx_quote_mut() else { - return Err(anyhow::anyhow!("Quote not found")); + let attestation = maybe_patch_report_data(attestation, report_data, patch_report_data, "quote"); + let Some(quote) = attestation.tdx_quote_bytes() else { + return Err(anyhow!("Quote not found")); }; Ok(GetQuoteResponse { - quote: quote.quote.to_vec(), - event_log: serde_json::to_string("e.event_log) - .context("Failed to serialize event log")?, + quote, + event_log: attestation.tdx_event_log_string().unwrap_or_default(), report_data: report_data.to_vec(), vm_config: vm_config.to_string(), }) @@ -50,14 +49,15 @@ pub fn simulated_attest_response( report_data: [u8; 64], patch_report_data: bool, ) -> AttestResponse { + let attestation = + maybe_patch_report_data(attestation, report_data, patch_report_data, "attest"); AttestResponse { - attestation: maybe_patch_report_data(attestation, report_data, patch_report_data, "attest") - .to_scale(), + attestation: VersionedAttestation::V1 { attestation }.to_bytes(), } } -pub fn simulated_info_attestation(attestation: &VersionedAttestation) -> Attestation { - attestation.clone().into_inner() +pub fn simulated_info_attestation(attestation: &VersionedAttestation) -> VersionedAttestation { + attestation.clone() } pub fn simulated_certificate_attestation( @@ -66,12 +66,13 @@ pub fn simulated_certificate_attestation( patch_report_data: bool, ) -> VersionedAttestation { let report_data = QuoteContentType::RaTlsCert.to_report_data(pubkey); - maybe_patch_report_data( + let attestation = maybe_patch_report_data( attestation, report_data, patch_report_data, "certificate_attestation", - ) + ); + VersionedAttestation::V1 { attestation } } fn maybe_patch_report_data( @@ -79,29 +80,61 @@ fn maybe_patch_report_data( report_data: [u8; 64], patch_report_data: bool, context: &str, -) -> VersionedAttestation { +) -> AttestationV1 { if !patch_report_data { warn!( context = context, requested_report_data = ?report_data, "simulator is preserving fixture report_data; returned attestation may not match the current request" ); - return attestation.clone(); + return attestation.clone().into_v1(); } + patch_v1_report_data(attestation.clone().into_v1(), report_data) +} - let VersionedAttestation::V0 { attestation } = attestation.clone(); - let mut attestation = attestation; - attestation.report_data = report_data; - if let Some(tdx_quote) = attestation.tdx_quote_mut() { - if tdx_quote.quote.len() >= TDX_QUOTE_REPORT_DATA_RANGE.end { - tdx_quote.quote[TDX_QUOTE_REPORT_DATA_RANGE].copy_from_slice(&report_data); - } else { - warn!( - "TDX quote too short to patch report_data ({} < {})", - tdx_quote.quote.len(), - TDX_QUOTE_REPORT_DATA_RANGE.end - ); +fn patch_v1_report_data(attestation: AttestationV1, report_data: [u8; 64]) -> AttestationV1 { + let AttestationV1 { + version, + platform, + stack, + } = attestation; + let platform = match platform { + PlatformEvidence::Tdx { + mut quote, + event_log, + } => { + if quote.len() >= TDX_QUOTE_REPORT_DATA_RANGE.end { + quote[TDX_QUOTE_REPORT_DATA_RANGE].copy_from_slice(&report_data); + } + PlatformEvidence::Tdx { quote, event_log } } + other => other, + }; + let stack = match stack { + StackEvidence::Dstack { + runtime_events, + config, + .. + } => StackEvidence::Dstack { + report_data: report_data.to_vec(), + runtime_events, + config, + }, + StackEvidence::DstackPod { + runtime_events, + config, + report_data_payload, + .. + } => StackEvidence::DstackPod { + report_data: report_data.to_vec(), + runtime_events, + config, + report_data_payload, + }, + }; + AttestationV1 { + version, + platform, + stack, } - VersionedAttestation::V0 { attestation } } diff --git a/guest-agent/src/backend.rs b/guest-agent/src/backend.rs index 4a7d4fa9..dc202729 100644 --- a/guest-agent/src/backend.rs +++ b/guest-agent/src/backend.rs @@ -5,11 +5,11 @@ use anyhow::{Context, Result}; use dstack_attest::emit_runtime_event; use dstack_guest_agent_rpc::{AttestResponse, GetQuoteResponse}; -use ra_rpc::Attestation; +use ra_tls::attestation::Attestation; use ra_tls::attestation::{QuoteContentType, VersionedAttestation}; pub trait PlatformBackend: Send + Sync { - fn attestation_for_info(&self) -> Result; + fn attestation_for_info(&self) -> Result; fn certificate_attestation(&self, pubkey: &[u8]) -> Result; fn quote_response(&self, report_data: [u8; 64], vm_config: &str) -> Result; fn attest_response(&self, report_data: [u8; 64]) -> Result; @@ -20,8 +20,10 @@ pub trait PlatformBackend: Send + Sync { pub struct RealPlatform; impl PlatformBackend for RealPlatform { - fn attestation_for_info(&self) -> Result { - Attestation::local().context("Failed to get local attestation") + fn attestation_for_info(&self) -> Result { + Ok(Attestation::local() + .context("Failed to get local attestation")? + .into_versioned()) } fn certificate_attestation(&self, pubkey: &[u8]) -> Result { @@ -46,7 +48,7 @@ impl PlatformBackend for RealPlatform { fn attest_response(&self, report_data: [u8; 64]) -> Result { let attestation = Attestation::quote(&report_data).context("Failed to get attestation")?; Ok(AttestResponse { - attestation: attestation.into_versioned().to_scale(), + attestation: attestation.into_versioned().to_bytes(), }) } diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 087139c4..c8eba51b 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -26,7 +26,9 @@ use k256::ecdsa::SigningKey; use or_panic::ResultOrPanic; use ra_rpc::{CallContext, RpcCall}; use ra_tls::{ - attestation::{QuoteContentType, DEFAULT_HASH_ALGORITHM}, + attestation::{ + QuoteContentType, TdxAttestationExt, VersionedAttestation, DEFAULT_HASH_ALGORITHM, + }, cert::{CertConfigV2, CertSigningRequestV2, Csr}, kdf::{derive_key, derive_p256_key_pair_from_bytes}, }; @@ -62,7 +64,7 @@ struct AppStateInner { } impl AppStateInner { - fn info_attestation(&self) -> Result { + fn info_attestation(&self) -> Result { self.platform.attestation_for_info() } @@ -189,19 +191,17 @@ pub struct InternalRpcHandler { pub async fn get_info(state: &AppState, external: bool) -> Result { let hide_tcb_info = external && !state.config().app_compose.public_tcbinfo; - let attestation = state.inner.info_attestation()?; + let versioned_attestation = state.inner.info_attestation()?; + let attestation = versioned_attestation.into_v1(); let app_info = attestation .decode_app_info(false) .context("Failed to decode app info")?; - let event_log = attestation - .tdx_quote() - .map(|q| &q.event_log[..]) - .unwrap_or_default(); + let event_log = attestation.tdx_event_log().unwrap_or_default(); let tcb_info = if hide_tcb_info { "".to_string() } else { let app_compose = state.config().app_compose.raw.clone(); - let td_report = match attestation.get_td10_report() { + let td_report = match attestation.td10_report() { Some(report) => json!({ "mrtd": hex::encode(report.mr_td), "rtmr0": hex::encode(report.rt_mr0), @@ -670,7 +670,10 @@ mod tests { Signature as Ed25519Signature, Verifier, VerifyingKey as Ed25519VerifyingKey, }; use k256::ecdsa::{Signature as K256Signature, VerifyingKey}; - use ra_tls::attestation::VersionedAttestation; + use ra_tls::attestation::{ + AttestationV1, PlatformEvidence, StackEvidence, VersionedAttestation, + TDX_QUOTE_REPORT_DATA_RANGE, + }; use sha2::Sha256; use std::collections::HashSet; use std::convert::TryFrom; @@ -813,34 +816,64 @@ pNs85uhOZE8z2jr8Pg== fn patch_report_data( attestation: &VersionedAttestation, report_data: [u8; 64], - ) -> VersionedAttestation { - let ra_tls::attestation::VersionedAttestation::V0 { attestation } = attestation.clone(); - let mut attestation = attestation; - attestation.report_data = report_data; - if let Some(tdx_quote) = attestation.tdx_quote_mut() { - if tdx_quote.quote.len() >= ra_tls::attestation::TDX_QUOTE_REPORT_DATA_RANGE.end { - tdx_quote.quote[ra_tls::attestation::TDX_QUOTE_REPORT_DATA_RANGE] - .copy_from_slice(&report_data); - } else { - tracing::warn!( - "TDX quote too short to patch report_data ({} < {})", - tdx_quote.quote.len(), - ra_tls::attestation::TDX_QUOTE_REPORT_DATA_RANGE.end - ); + ) -> AttestationV1 { + let attestation = attestation.clone().into_v1(); + let AttestationV1 { + version, + platform, + stack, + } = attestation; + let platform = match platform { + PlatformEvidence::Tdx { + mut quote, + event_log, + } => { + if quote.len() >= TDX_QUOTE_REPORT_DATA_RANGE.end { + quote[TDX_QUOTE_REPORT_DATA_RANGE].copy_from_slice(&report_data); + } + PlatformEvidence::Tdx { quote, event_log } } + other => other, + }; + let stack = match stack { + StackEvidence::Dstack { + runtime_events, + config, + .. + } => StackEvidence::Dstack { + report_data: report_data.to_vec(), + runtime_events, + config, + }, + StackEvidence::DstackPod { + runtime_events, + config, + report_data_payload, + .. + } => StackEvidence::DstackPod { + report_data: report_data.to_vec(), + runtime_events, + config, + report_data_payload, + }, + }; + AttestationV1 { + version, + platform, + stack, } - ra_tls::attestation::VersionedAttestation::V0 { attestation } } impl PlatformBackend for TestSimulatorPlatform { - fn attestation_for_info(&self) -> Result { - Ok(self.attestation.clone().into_inner()) + fn attestation_for_info(&self) -> Result { + Ok(self.attestation.clone()) } fn certificate_attestation(&self, pubkey: &[u8]) -> Result { let report_data = ra_tls::attestation::QuoteContentType::RaTlsCert.to_report_data(pubkey); - Ok(patch_report_data(&self.attestation, report_data)) + let attestation = patch_report_data(&self.attestation, report_data); + Ok(VersionedAttestation::V1 { attestation }) } fn quote_response( @@ -848,24 +881,25 @@ pNs85uhOZE8z2jr8Pg== report_data: [u8; 64], vm_config: &str, ) -> Result { - let ra_tls::attestation::VersionedAttestation::V0 { attestation } = - patch_report_data(&self.attestation, report_data); - let mut attestation = attestation; - let Some(quote) = attestation.tdx_quote_mut() else { + let attestation = patch_report_data(&self.attestation, report_data); + let Some(quote) = attestation.platform.tdx_quote().map(ToOwned::to_owned) else { return Err(anyhow::anyhow!("Quote not found")); }; Ok(GetQuoteResponse { - quote: quote.quote.to_vec(), - event_log: serde_json::to_string("e.event_log) - .context("Failed to serialize event log")?, + quote, + event_log: serde_json::to_string( + attestation.tdx_event_log().unwrap_or_default(), + ) + .unwrap_or_default(), report_data: report_data.to_vec(), vm_config: vm_config.to_string(), }) } fn attest_response(&self, report_data: [u8; 64]) -> Result { + let attestation = patch_report_data(&self.attestation, report_data); Ok(AttestResponse { - attestation: patch_report_data(&self.attestation, report_data).to_scale(), + attestation: VersionedAttestation::V1 { attestation }.to_bytes(), }) } @@ -881,7 +915,7 @@ pNs85uhOZE8z2jr8Pg== cert_client: dummy_cert_client, demo_cert: RwLock::new(String::new()), platform: Arc::new(TestSimulatorPlatform { - attestation: VersionedAttestation::from_scale( + attestation: VersionedAttestation::from_bytes( &std::fs::read(temp_attestation_file.path()).unwrap(), ) .unwrap(), diff --git a/kms/src/main_service.rs b/kms/src/main_service.rs index 9965fc8d..b738cd29 100644 --- a/kms/src/main_service.rs +++ b/kms/src/main_service.rs @@ -397,7 +397,7 @@ impl KmsRpc for RpcHandler { let attestation = csr .attestation .clone() - .into_inner() + .into_v1() .verify_with_ra_pubkey(&csr.pubkey, self.state.config.pccs_url.as_deref()) .await .context("Quote verification failed")?; diff --git a/kms/src/main_service/upgrade_authority.rs b/kms/src/main_service/upgrade_authority.rs index f3082ab5..e98b436f 100644 --- a/kms/src/main_service/upgrade_authority.rs +++ b/kms/src/main_service/upgrade_authority.rs @@ -76,10 +76,10 @@ pub(crate) async fn local_kms_boot_info(pccs_url: Option<&str>) -> Result "dstack-tdx", + PlatformEvidence::GcpTdx => "dstack-gcp-tdx", + PlatformEvidence::NitroEnclave => "dstack-nitro-enclave", + } + .to_string(); let verified = attestation + .into_v1() .verify(pccs_url.as_deref()) .await .context("Failed to verify attestation")?; @@ -195,7 +197,7 @@ impl Keys { let response = app_attest(report_data.to_vec()) .await .context("Failed to get quote")?; - let attestation = VersionedAttestation::from_scale(&response.attestation) + let attestation = VersionedAttestation::from_bytes(&response.attestation) .context("Invalid attestation")?; // Sign WWW server cert with KMS cert @@ -376,7 +378,7 @@ async fn gen_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result<(String, .await .context("Failed to get quote")?; let attestation = - VersionedAttestation::from_scale(&response.attestation).context("Invalid attestation")?; + VersionedAttestation::from_bytes(&response.attestation).context("Invalid attestation")?; let req = CertRequest::builder() .subject("RA-TLS TEMP Cert") .attestation(&attestation) diff --git a/ra-rpc/src/client.rs b/ra-rpc/src/client.rs index 1f68ad83..3d91abe7 100644 --- a/ra-rpc/src/client.rs +++ b/ra-rpc/src/client.rs @@ -136,6 +136,7 @@ impl RaClient { None => None, Some(attestation) => { let verified_attestation = attestation + .into_v1() .verify_with_ra_pubkey(cert.public_key().raw, self.pccs_url.as_deref()) .await .context("Failed to verify the attestation report")?; diff --git a/ra-rpc/src/rocket_helper.rs b/ra-rpc/src/rocket_helper.rs index b3074a4c..87e6872e 100644 --- a/ra-rpc/src/rocket_helper.rs +++ b/ra-rpc/src/rocket_helper.rs @@ -540,6 +540,7 @@ pub async fn handle_prpc_impl>( .raw .to_vec(); let verified = attestation + .into_v1() .verify_with_ra_pubkey(&pubkey, quote_verifier.pccs_url.as_deref()) .await .context("invalid quote")?; diff --git a/ra-tls/src/attestation.rs b/ra-tls/src/attestation.rs index fcd6815a..dec7d92f 100644 --- a/ra-tls/src/attestation.rs +++ b/ra-tls/src/attestation.rs @@ -10,26 +10,25 @@ use crate::{oids, traits::CertExt}; use anyhow::{Context, Result}; /// Extract attestation from x509 certificate -pub fn from_der(cert: &[u8]) -> Result> { +pub fn from_der(cert: &[u8]) -> Result> { let (_, cert) = x509_parser::parse_x509_certificate(cert).context("Failed to parse certificate")?; from_cert(&cert) } /// Extract attestation from a certificate -pub fn from_cert(cert: &impl CertExt) -> Result> { +pub fn from_cert(cert: &impl CertExt) -> Result> { from_ext_getter(|oid| cert.get_extension_bytes(oid)) } /// Extract attestation from a certificate extension getter pub fn from_ext_getter( get_ext: impl Fn(&[u64]) -> Result>>, -) -> Result> { +) -> Result> { // Try to detect attestation mode from certificate extension if let Some(attestation_bytes) = get_ext(oids::PHALA_RATLS_ATTESTATION)? { - let VersionedAttestation::V0 { attestation } = - VersionedAttestation::from_scale(&attestation_bytes) - .context("Failed to decode attestation from cert extension")?; + let attestation = VersionedAttestation::from_bytes(&attestation_bytes) + .context("Failed to decode attestation from cert extension")?; return Ok(Some(attestation)); } // Backward compatibility: if PHALA_RATLS_ATTESTATION @@ -39,6 +38,7 @@ pub fn from_ext_getter( let raw_event_log = get_ext(oids::PHALA_RATLS_EVENT_LOG)?.context("TDX event log missing")?; Ok(Some( Attestation::from_tdx_quote(tdx_quote, &raw_event_log) - .context("Failed to create attestation from TDX quote")?, + .context("Failed to create attestation from TDX quote")? + .into_versioned(), )) } diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index 4527573c..f53fea96 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -88,7 +88,7 @@ impl CaCert { .context("Failed to parse signature")?; let cfg = &csr.config; let app_info = if cfg.ext_app_info { - Some(csr.attestation.decode_app_info(false)?) + Some(csr.attestation.clone().into_v1().decode_app_info(false)?) } else { None }; @@ -388,7 +388,7 @@ impl CertRequest<'_, Key> { add_ext(&mut params, PHALA_RATLS_CERT_USAGE, usage); } if let Some(ver_att) = self.attestation { - let attestation_bytes = ver_att.clone().into_stripped().to_scale(); + let attestation_bytes = ver_att.clone().into_stripped().to_bytes(); add_ext(&mut params, PHALA_RATLS_ATTESTATION, &attestation_bytes); } if let Some(ca_level) = self.ca_level { diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index af9474be..ae612227 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -389,7 +389,7 @@ impl CvmVerifier { pub async fn verify(&self, request: VerificationRequest) -> Result { let attestation = if let Some(attestation) = &request.attestation { - VersionedAttestation::from_scale(attestation).context("Failed to decode attestaion")? + VersionedAttestation::from_bytes(attestation).context("Failed to decode attestaion")? } else if let Some(tdx_quote) = request.quote { let event_log = request .event_log @@ -413,9 +413,8 @@ impl CvmVerifier { rtmr_debug: None, }; - let attestation = attestation.into_inner(); let debug = request.debug.unwrap_or(false); - let verified = attestation.verify(self.pccs_url.as_deref()).await; + let verified = attestation.into_v1().verify(self.pccs_url.as_deref()).await; let verified_attestation = match verified { Ok(att) => { details.quote_verified = true;