diff --git a/dstack/Cargo.lock b/dstack/Cargo.lock index 6f4b039c2..7d087f93c 100644 --- a/dstack/Cargo.lock +++ b/dstack/Cargo.lock @@ -2008,9 +2008,12 @@ dependencies = [ "anyhow", "cc-eventlog", "clap", + "dcap-qvl", "dstack-guest-agent", "dstack-guest-agent-rpc", "dstack-types", + "hex", + "mock-attestation", "ra-rpc", "ra-tls", "rocket", diff --git a/dstack/crates/mock-attestation/src/tdx.rs b/dstack/crates/mock-attestation/src/tdx.rs index 9ae50a513..f3eb703fe 100644 --- a/dstack/crates/mock-attestation/src/tdx.rs +++ b/dstack/crates/mock-attestation/src/tdx.rs @@ -3,22 +3,25 @@ // SPDX-License-Identifier: Apache-2.0 use anyhow::Result; +use dcap_qvl::QuoteCollateralV3; use dcap_qvl::quote::{ AuthData, AuthDataV4, CertificationData, Data, EnclaveReport, Header, QEReportCertificationData, Quote, Report, TDReport10, }; -use dcap_qvl::QuoteCollateralV3; -use p256::ecdsa::{signature::Signer, Signature, SigningKey}; -use p256::pkcs8::DecodePrivateKey; +use p256::ecdsa::{Signature, SigningKey, signature::Signer}; +use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey}; use rcgen::{ BasicConstraints, Certificate, CertificateParams, CertificateRevocationListParams, CertifiedKey, CustomExtension, DnType, ExtendedKeyUsagePurpose, IsCa, KeyIdMethod, KeyPair, - KeyUsagePurpose, SerialNumber, + KeyUsagePurpose, PKCS_ECDSA_P256_SHA256, RemoteKeyPair, SerialNumber, SignatureAlgorithm, }; use scale::Encode; use serde_json::json; use sha2::{Digest, Sha256}; -use time::{Duration, OffsetDateTime}; +use time::OffsetDateTime; + +const MOCK_PKI_NOT_BEFORE: i64 = 1_577_836_800; // 2020-01-01T00:00:00Z +const MOCK_PKI_NOT_AFTER: i64 = 4_102_444_800; // 2100-01-01T00:00:00Z const INTEL_QE_VENDOR_ID: [u8; 16] = [ 0x93, 0x9a, 0x72, 0x33, 0xf7, 0x9c, 0x4c, 0xa9, 0x94, 0x0a, 0x0d, 0xb3, 0x95, 0x7f, 0x06, 0x07, @@ -26,16 +29,47 @@ const INTEL_QE_VENDOR_ID: [u8; 16] = [ pub struct TdxGenerator { root: Certificate, - root_key: KeyPair, + root_signing_key: SigningKey, pck: Certificate, - pck_key: KeyPair, + pck_key: SigningKey, tcb_signer: Certificate, - tcb_signer_key: KeyPair, + tcb_signer_key: SigningKey, qe_signer: Certificate, - qe_signer_key: KeyPair, + qe_signer_key: SigningKey, root_crl: Vec, } +struct DeterministicP256KeyPair { + key: SigningKey, + public_key: Vec, +} + +impl DeterministicP256KeyPair { + fn new(key: SigningKey) -> Self { + let public_key = key + .verifying_key() + .to_encoded_point(false) + .as_bytes() + .to_vec(); + Self { key, public_key } + } +} + +impl RemoteKeyPair for DeterministicP256KeyPair { + fn public_key(&self) -> &[u8] { + &self.public_key + } + + fn sign(&self, message: &[u8]) -> Result, rcgen::Error> { + let signature: Signature = self.key.sign(message); + Ok(signature.to_der().as_bytes().to_vec()) + } + + fn algorithm(&self) -> &'static SignatureAlgorithm { + &PKCS_ECDSA_P256_SHA256 + } +} + pub struct TdxEvidence { pub quote: Vec, pub collateral: QuoteCollateralV3, @@ -47,10 +81,13 @@ impl TdxGenerator { } pub fn from_seed(seed: [u8; 32]) -> Result { - let CertifiedKey { - cert: root, - key_pair: root_key, - } = make_root(&seed)?; + let ( + CertifiedKey { + cert: root, + key_pair: root_key, + }, + root_signing_key, + ) = make_root(&seed)?; let (pck, pck_key) = make_leaf( "Mock Intel SGX PCK Certificate", "tdx-pck", @@ -75,10 +112,9 @@ impl TdxGenerator { &root_key, false, )?; - let now = OffsetDateTime::now_utc(); let root_crl = CertificateRevocationListParams { - this_update: now - Duration::days(1), - next_update: now + Duration::days(30), + this_update: fixed_time(MOCK_PKI_NOT_BEFORE)?, + next_update: fixed_time(MOCK_PKI_NOT_AFTER)?, crl_number: SerialNumber::from(1u64), issuing_distribution_point: None, revoked_certs: Vec::new(), @@ -89,7 +125,7 @@ impl TdxGenerator { .to_vec(); Ok(Self { root, - root_key, + root_signing_key, pck, pck_key, tcb_signer, @@ -107,7 +143,10 @@ impl TdxGenerator { self.root.pem() } pub fn root_key_pem(&self) -> String { - self.root_key.serialize_pem() + self.root_signing_key + .to_pkcs8_pem(Default::default()) + .expect("P-256 key serialization") + .to_string() } pub fn sample_collateral(&self) -> Result { @@ -166,8 +205,7 @@ impl TdxGenerator { .encode() .try_into() .map_err(|bytes: Vec| anyhow::anyhow!("invalid QE report size {}", bytes.len()))?; - let pck_key = signing_key(&self.pck_key)?; - let qe_sig: Signature = pck_key.sign(&qe_report_bytes); + let qe_sig: Signature = self.pck_key.sign(&qe_report_bytes); let pck_chain = format!("{}{}", self.pck.pem(), self.root.pem()).into_bytes(); let qe_certification = QEReportCertificationData { @@ -263,8 +301,16 @@ impl TdxGenerator { } } -fn make_root(seed: &[u8; 32]) -> Result { - let key_pair = crate::p256_key(seed, "tdx-root")?; +fn deterministic_key_pair(seed: &[u8; 32], label: &str) -> Result<(KeyPair, SigningKey)> { + let serialized = crate::p256_key(seed, label)?; + let signing_key = SigningKey::from_pkcs8_pem(&serialized.serialize_pem())?; + let key_pair = + KeyPair::from_remote(Box::new(DeterministicP256KeyPair::new(signing_key.clone())))?; + Ok((key_pair, signing_key)) +} + +fn make_root(seed: &[u8; 32]) -> Result<(CertifiedKey, SigningKey)> { + let (key_pair, signing_key) = deterministic_key_pair(seed, "tdx-root")?; let mut params = cert_params("Mock Intel SGX Root CA")?; params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); params.key_usages.extend([ @@ -273,7 +319,7 @@ fn make_root(seed: &[u8; 32]) -> Result { KeyUsagePurpose::CrlSign, ]); let cert = params.self_signed(&key_pair)?; - Ok(CertifiedKey { cert, key_pair }) + Ok((CertifiedKey { cert, key_pair }, signing_key)) } fn make_leaf( @@ -283,8 +329,8 @@ fn make_leaf( root: &Certificate, root_key: &KeyPair, pck: bool, -) -> Result<(Certificate, KeyPair)> { - let key = crate::p256_key(seed, label)?; +) -> Result<(Certificate, SigningKey)> { + let (key, signing_key) = deterministic_key_pair(seed, label)?; let mut params = cert_params(name)?; params.key_usages.push(KeyUsagePurpose::DigitalSignature); params @@ -294,19 +340,22 @@ fn make_leaf( params.custom_extensions.push(pck_extension()); } let cert = params.signed_by(&key, root, root_key)?; - Ok((cert, key)) + Ok((cert, signing_key)) } fn cert_params(name: &str) -> Result { let mut params = CertificateParams::new(vec!["mock.dstack.invalid".into()])?; params.distinguished_name.push(DnType::CommonName, name); params.serial_number = Some(SerialNumber::from(42u64)); - let now = OffsetDateTime::now_utc(); - params.not_before = now - Duration::days(1); - params.not_after = now + Duration::days(30); + params.not_before = fixed_time(MOCK_PKI_NOT_BEFORE)?; + params.not_after = fixed_time(MOCK_PKI_NOT_AFTER)?; Ok(params) } +fn fixed_time(timestamp: i64) -> Result { + Ok(OffsetDateTime::from_unix_timestamp(timestamp)?) +} + fn pck_extension() -> CustomExtension { fn oid(writer: yasna::DERWriter, oid: &[u64]) { writer.write_oid(&yasna::models::ObjectIdentifier::from_slice(oid)); @@ -347,11 +396,8 @@ fn pck_extension() -> CustomExtension { CustomExtension::from_oid_content(&[1, 2, 840, 113741, 1, 13, 1], der) } -fn signing_key(key: &KeyPair) -> Result { - Ok(SigningKey::from_pkcs8_pem(&key.serialize_pem())?) -} -fn sign_raw(key: &KeyPair, message: &[u8]) -> Result> { - let sig: Signature = signing_key(key)?.sign(message); +fn sign_raw(key: &SigningKey, message: &[u8]) -> Result> { + let sig: Signature = key.sign(message); Ok(sig.to_bytes().to_vec()) } @@ -359,6 +405,26 @@ fn sign_raw(key: &KeyPair, message: &[u8]) -> Result> { mod tests { use super::*; + #[test] + fn seeded_hierarchies_are_cross_process_compatible() { + let first = TdxGenerator::from_seed([0x31; 32]).unwrap(); + let second = TdxGenerator::from_seed([0x31; 32]).unwrap(); + assert_eq!(first.root_ca_der(), second.root_ca_der()); + assert_eq!(first.root_crl_der(), second.root_crl_der()); + + let evidence = first.attest([0x42; 64]).unwrap(); + let collateral = second.sample_collateral().unwrap(); + assert_eq!(evidence.collateral, collateral); + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + dcap_qvl::verify::QuoteVerifier::new(second.root_ca_der()) + .verify(&evidence.quote, &collateral, now) + .unwrap(); + } + #[test] fn generated_quote_passes_real_qvl_and_negative_cases_fail() { let generator = TdxGenerator::new().unwrap(); @@ -380,13 +446,14 @@ mod tests { ); let mut tampered = evidence.quote.clone(); tampered[100] ^= 1; - assert!(verifier - .verify(&tampered, &evidence.collateral, now) - .is_err()); - assert!(crate::ensure_report_data( - &verified.report.as_td10().unwrap().report_data, - &[0x24; 64] - ) - .is_err()); + assert!( + verifier + .verify(&tampered, &evidence.collateral, now) + .is_err() + ); + assert!( + crate::ensure_report_data(&verified.report.as_td10().unwrap().report_data, &[0x24; 64]) + .is_err() + ); } } diff --git a/dstack/dstack-attest/src/attestation.rs b/dstack/dstack-attest/src/attestation.rs index 4b6ef17a2..5fabb8176 100644 --- a/dstack/dstack-attest/src/attestation.rs +++ b/dstack/dstack-attest/src/attestation.rs @@ -179,6 +179,21 @@ impl AttestationVerifier { }) } + /// Construct a verifier with a development-only external TDX trust root. + /// + /// Callers must require an explicit insecure opt-in and surface the result + /// as simulated evidence; production verification must use `new_prod`. + pub fn new_with_tdx_root( + collateral_urls: Option<&CollateralUrls>, + root_ca: &[u8], + ) -> Result { + validate_x509_certificate(root_ca, "TDX")?; + let mut verifier = Self::new_prod(collateral_urls)?; + verifier.tdx = dcap_qvl::verify::QuoteVerifier::new(tdx_root_der(root_ca.to_vec())?); + verifier.external_trust_anchors = true; + Ok(verifier) + } + /// Whether this verifier accepts development-only external trust roots. /// /// A true value must be surfaced as simulated evidence by every caller; @@ -2721,7 +2736,10 @@ mod tests { let content = b"test content"; let report_data = content_type.to_report_data(content); - assert_eq!(hex::encode(report_data), "7ea0b744ed5e9c0c83ff9f575668e1697652cd349f2027cdf26f918d4c53e8cd50b5ea9b449b4c3d50e20ae00ec29688d5a214e8daff8a10041f5d624dae8a01"); + assert_eq!( + hex::encode(report_data), + "7ea0b744ed5e9c0c83ff9f575668e1697652cd349f2027cdf26f918d4c53e8cd50b5ea9b449b4c3d50e20ae00ec29688d5a214e8daff8a10041f5d624dae8a01" + ); // Test SHA-256 let result = content_type diff --git a/dstack/dstack-types/src/lib.rs b/dstack/dstack-types/src/lib.rs index 141ed3bb9..cde2b544f 100644 --- a/dstack/dstack-types/src/lib.rs +++ b/dstack/dstack-types/src/lib.rs @@ -887,6 +887,12 @@ pub struct SysConfig { /// for fields that are absent. #[serde(default, skip_serializing_if = "Option::is_none")] pub collateral_urls: Option, + /// Development-only opt-in for a host-supplied TDX attestation trust root. + #[serde(default)] + pub insecure_allow_external_attestation_trust_anchor: bool, + /// Public PEM TDX root used only when the explicit insecure opt-in is set. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tdx_attestation_root_ca: Option, /// Optional NVIDIA attestation collateral proxy. When present, nvattest /// fetches both OCSP responses and RIM documents through this endpoint. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -934,6 +940,9 @@ pub struct TeeSimulatorConfig { /// Base URL used in mock collateral certificates (AIA/CRL). #[serde(default, skip_serializing_if = "Option::is_none")] pub collateral_base_url: Option, + /// Exact public PEM root shared with verifiers of simulated TDX evidence. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tdx_root_ca: Option, /// MrConfigV3 document used to generate mock platform evidence. #[serde(default, skip_serializing_if = "Option::is_none")] pub mr_config: Option, diff --git a/dstack/dstack-util/src/system_setup.rs b/dstack/dstack-util/src/system_setup.rs index 6f258d34b..e8fd2b0f6 100644 --- a/dstack/dstack-util/src/system_setup.rs +++ b/dstack/dstack-util/src/system_setup.rs @@ -65,6 +65,23 @@ use serde_human_bytes as hex_bytes; use serde_json::Value; use tpm_attest::{self as tpm, TpmContext}; +fn attestation_verifier(sys_config: &SysConfig) -> Result> { + let collateral_urls = sys_config.collateral_urls(); + let Some(root_ca) = sys_config.tdx_attestation_root_ca.as_deref() else { + return Ok(Arc::new(AttestationVerifier::new_prod(Some( + &collateral_urls, + ))?)); + }; + anyhow::ensure!( + sys_config.insecure_allow_external_attestation_trust_anchor, + "external TDX attestation trust root requires explicit insecure opt-in" + ); + Ok(Arc::new(AttestationVerifier::new_with_tdx_root( + Some(&collateral_urls), + root_ca.as_bytes(), + )?)) +} + async fn sign_cert_request( cert_client: &CertRequestClient, key: &KeyPair, @@ -478,8 +495,7 @@ impl<'a> GatewayContext<'a> { .map(|d| d.as_secs()) .unwrap_or(0); let cert_not_after = now + CERT_VALIDITY_SECS; - let collateral_urls = self.shared.sys_config.collateral_urls(); - let verifier = Arc::new(AttestationVerifier::new_prod(Some(&collateral_urls))?); + let verifier = attestation_verifier(&self.shared.sys_config)?; let cert_client = CertRequestClient::create( self.keys, verifier, @@ -2029,8 +2045,7 @@ impl<'a> Stage0<'a> { .context("Failed to get temp ca cert")? }; let cert_pair = generate_ra_cert(tmp_ca.temp_ca_cert.clone(), tmp_ca.temp_ca_key.clone())?; - let collateral_urls = self.shared.sys_config.collateral_urls(); - let attestation_verifier = Arc::new(AttestationVerifier::new_prod(Some(&collateral_urls))?); + let attestation_verifier = attestation_verifier(&self.shared.sys_config)?; let ra_client = RaClientConfig::builder() .tls_no_check(false) .tls_built_in_root_certs(false) diff --git a/dstack/guest-agent-simulator/Cargo.toml b/dstack/guest-agent-simulator/Cargo.toml index 6fad16705..8d7fd4c16 100644 --- a/dstack/guest-agent-simulator/Cargo.toml +++ b/dstack/guest-agent-simulator/Cargo.toml @@ -27,3 +27,6 @@ dstack-guest-agent = { path = "../guest-agent" } dstack-guest-agent-rpc.workspace = true dstack-types.workspace = true cc-eventlog.workspace = true +dcap-qvl.workspace = true +hex.workspace = true +mock-attestation = { path = "../crates/mock-attestation" } diff --git a/dstack/guest-agent-simulator/src/main.rs b/dstack/guest-agent-simulator/src/main.rs index c2bf89bf1..30456f3a0 100644 --- a/dstack/guest-agent-simulator/src/main.rs +++ b/dstack/guest-agent-simulator/src/main.rs @@ -9,11 +9,13 @@ use std::sync::Arc; use anyhow::{Context, Result}; use clap::Parser; use dstack_guest_agent::{ + AppState, backend::PlatformBackend, config::{self, Config}, - run_server, AppState, + run_server, }; use dstack_guest_agent_rpc::{AttestResponse, GetQuoteResponse}; +use mock_attestation::tdx::TdxGenerator; use ra_tls::attestation::VersionedAttestation; use serde::Deserialize; use tracing::warn; @@ -37,6 +39,8 @@ struct SimulatorSettings { attestation_file: String, #[serde(default = "default_patch_report_data")] patch_report_data: bool, + #[serde(default)] + mock_attestation_seed: Option, } #[derive(Debug, Clone, Deserialize)] @@ -49,14 +53,25 @@ struct SimulatorCoreConfig { struct SimulatorPlatform { attestation: VersionedAttestation, patch_report_data: bool, + generator: Option, } impl SimulatorPlatform { - fn new(attestation: VersionedAttestation, patch_report_data: bool) -> Self { - Self { + fn new( + attestation: VersionedAttestation, + patch_report_data: bool, + mock_attestation_seed: Option<&str>, + ) -> Result { + let generator = mock_attestation_seed + .map(mock_attestation::parse_seed) + .transpose()? + .map(TdxGenerator::from_seed) + .transpose()?; + Ok(Self { attestation, patch_report_data, - } + generator, + }) } } @@ -74,6 +89,7 @@ impl PlatformBackend for SimulatorPlatform { &self.attestation, pubkey, self.patch_report_data, + self.generator.as_ref(), ) } @@ -83,18 +99,24 @@ impl PlatformBackend for SimulatorPlatform { report_data, vm_config, self.patch_report_data, + self.generator.as_ref(), ) } fn attest_response(&self, report_data: [u8; 64]) -> Result { - simulator::simulated_attest_response(&self.attestation, report_data, self.patch_report_data) + simulator::simulated_attest_response( + &self.attestation, + report_data, + self.patch_report_data, + self.generator.as_ref(), + ) } } #[rocket::main] async fn main() -> Result<()> { { - use tracing_subscriber::{fmt, EnvFilter}; + use tracing_subscriber::{EnvFilter, fmt}; let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); fmt().with_env_filter(filter).with_ansi(false).init(); } @@ -107,12 +129,17 @@ async fn main() -> Result<()> { warn!( attestation_file = %sim_config.simulator.attestation_file, patch_report_data = sim_config.simulator.patch_report_data, + signed_quotes = sim_config.simulator.mock_attestation_seed.is_some(), "starting dstack guest-agent simulator" ); if sim_config.simulator.patch_report_data { - warn!("simulator will rewrite report_data to match requests; quote verification may fail against the original fixture signature"); + warn!( + "simulator will rewrite report_data to match requests; quote verification may fail against the original fixture signature" + ); } else { - warn!("simulator will preserve fixture report_data; cert/key binding and requested report_data may not match"); + warn!( + "simulator will preserve fixture report_data; cert/key binding and requested report_data may not match" + ); } let attestation = simulator::load_versioned_attestation(&sim_config.simulator.attestation_file)?; @@ -121,7 +148,8 @@ async fn main() -> Result<()> { Arc::new(SimulatorPlatform::new( attestation, sim_config.simulator.patch_report_data, - )), + sim_config.simulator.mock_attestation_seed.as_deref(), + )?), ) .await .context("Failed to create simulator app state")?; @@ -131,6 +159,7 @@ async fn main() -> Result<()> { #[cfg(test)] mod tests { use super::*; + use ra_tls::attestation::TdxAttestationExt; fn load_fixture_platform() -> SimulatorPlatform { let fixture = simulator::load_versioned_attestation( @@ -138,7 +167,7 @@ mod tests { .join("../guest-agent/fixtures/attestation.bin"), ) .expect("fixture attestation should load"); - SimulatorPlatform::new(fixture, true) + SimulatorPlatform::new(fixture, true, None).unwrap() } #[test] @@ -162,6 +191,34 @@ mod tests { assert_eq!(patched.report_data().unwrap(), report_data); } + #[test] + fn seeded_simulator_resigns_certificate_attestation() { + let fixture = simulator::load_versioned_attestation( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../guest-agent/fixtures/attestation.bin"), + ) + .unwrap(); + let seed = [0x5a; 32]; + let platform = SimulatorPlatform::new(fixture, true, Some(&hex::encode(seed))).unwrap(); + let attestation = platform + .certificate_attestation(b"test-public-key") + .unwrap() + .into_v1(); + let quote = attestation.tdx_quote_bytes().unwrap(); + let generator = TdxGenerator::from_seed(seed).unwrap(); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + dcap_qvl::verify::QuoteVerifier::new(generator.root_ca_der()) + .verify("e, &generator.sample_collateral().unwrap(), now) + .unwrap(); + assert_eq!( + attestation.report_data().unwrap(), + ra_tls::attestation::QuoteContentType::RaTlsCert.to_report_data(b"test-public-key") + ); + } + #[test] fn simulator_can_preserve_fixture_report_data() { let fixture = simulator::load_versioned_attestation( @@ -170,7 +227,7 @@ mod tests { ) .expect("fixture attestation should load"); let original = fixture.clone().into_v1().report_data().unwrap(); - let platform = SimulatorPlatform::new(fixture, false); + let platform = SimulatorPlatform::new(fixture, false, None).unwrap(); let report_data = [0x5a; 64]; let response = platform.attest_response(report_data).unwrap(); let patched = VersionedAttestation::from_bytes(&response.attestation) diff --git a/dstack/guest-agent-simulator/src/simulator.rs b/dstack/guest-agent-simulator/src/simulator.rs index 23a74d0ec..f7fc6a91b 100644 --- a/dstack/guest-agent-simulator/src/simulator.rs +++ b/dstack/guest-agent-simulator/src/simulator.rs @@ -4,10 +4,12 @@ use std::path::Path; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result, anyhow}; +use dcap_qvl::quote::Quote; use dstack_guest_agent_rpc::{AttestResponse, GetQuoteResponse}; +use mock_attestation::tdx::TdxGenerator; use ra_tls::attestation::{ - AttestationV1, QuoteContentType, TdxAttestationExt, VersionedAttestation, + AttestationV1, PlatformEvidence, QuoteContentType, TdxAttestationExt, VersionedAttestation, }; use std::fs; use tracing::warn; @@ -29,8 +31,15 @@ pub fn simulated_quote_response( report_data: [u8; 64], vm_config: &str, patch_report_data: bool, + generator: Option<&TdxGenerator>, ) -> Result { - let attestation = maybe_patch_report_data(attestation, report_data, patch_report_data, "quote"); + let attestation = prepare_attestation( + attestation, + report_data, + patch_report_data, + generator, + "quote", + )?; let Some(quote) = attestation.tdx_quote_bytes() else { return Err(anyhow!("Quote not found")); }; @@ -52,9 +61,15 @@ pub fn simulated_attest_response( attestation: &VersionedAttestation, report_data: [u8; 64], patch_report_data: bool, + generator: Option<&TdxGenerator>, ) -> Result { - let mut attestation = - maybe_patch_report_data(attestation, report_data, patch_report_data, "attest"); + let mut attestation = prepare_attestation( + attestation, + report_data, + patch_report_data, + generator, + "attest", + )?; if let Some(event_log) = attestation.platform.tdx_event_log_mut() { cc_eventlog::tdx::fill_v2_preimages(event_log); } @@ -71,17 +86,56 @@ pub fn simulated_certificate_attestation( attestation: &VersionedAttestation, pubkey: &[u8], patch_report_data: bool, + generator: Option<&TdxGenerator>, ) -> Result { let report_data = QuoteContentType::RaTlsCert.to_report_data(pubkey); - let attestation = maybe_patch_report_data( + let attestation = prepare_attestation( attestation, report_data, patch_report_data, + generator, "certificate_attestation", - ); + )?; Ok(VersionedAttestation::V1 { attestation }) } +fn prepare_attestation( + attestation: &VersionedAttestation, + report_data: [u8; 64], + patch_report_data: bool, + generator: Option<&TdxGenerator>, + context: &str, +) -> Result { + let Some(generator) = generator else { + return Ok(maybe_patch_report_data( + attestation, + report_data, + patch_report_data, + context, + )); + }; + let mut attestation = attestation.clone().into_v1().with_report_data(report_data); + let quote = attestation + .platform + .tdx_quote() + .context("TDX quote is unavailable in simulator fixture")?; + let quote = Quote::parse(quote).context("invalid simulator fixture TDX quote")?; + let report = quote + .report + .as_td10() + .context("simulator fixture does not contain a TDX 1.0 report")?; + let evidence = generator.attest_with_measurements( + report_data, + report.mr_td, + [report.rt_mr0, report.rt_mr1, report.rt_mr2, report.rt_mr3], + )?; + match &mut attestation.platform { + PlatformEvidence::Tdx { quote, .. } => *quote = evidence.quote, + _ => return Err(anyhow!("seeded simulator requires dstack TDX evidence")), + } + Ok(attestation) +} + fn maybe_patch_report_data( attestation: &VersionedAttestation, report_data: [u8; 64], diff --git a/dstack/vmm/src/app.rs b/dstack/vmm/src/app.rs index 690b7c7af..43a7b60b9 100644 --- a/dstack/vmm/src/app.rs +++ b/dstack/vmm/src/app.rs @@ -4,7 +4,7 @@ use crate::config::{Config, Networking, ProcessAnnotation, Protocol}; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use bon::Builder; use dstack_kms_rpc::kms_client::KmsClient; use dstack_types::mr_config::MrConfigV3; @@ -1328,6 +1328,28 @@ pub(crate) fn make_sys_config( "host_api_url": format!("vsock://2:{}/api", cfg.host_api.port), "vm_config": serde_json::to_string(&vm_config)?, }); + if manifest.simulated_tee == Some(dstack_types::TeeVariant::DstackTdx) { + let simulator = cfg + .cvm + .tee_simulator + .as_ref() + .context("TEE simulator credentials are unavailable")?; + let seed = simulator + .mock_attestation_seed + .as_deref() + .context("TEE simulator seed is unavailable")?; + let seed: [u8; 32] = hex::decode(seed) + .context("invalid TEE simulator seed")? + .try_into() + .map_err(|_| anyhow!("TEE simulator seed must contain 32 bytes"))?; + let root_ca = match simulator.tdx_root_ca.as_deref() { + Some(root_ca) => root_ca.to_string(), + None => mock_attestation::tdx::TdxGenerator::from_seed(seed)?.root_ca_pem(), + }; + sys_config["insecure_allow_external_attestation_trust_anchor"] = + serde_json::Value::Bool(true); + sys_config["tdx_attestation_root_ca"] = serde_json::Value::String(root_ca); + } if let Some(mr_config) = mr_config { MrConfigV3::from_document(&mr_config).context("Invalid mr_config document")?; sys_config["mr_config"] = serde_json::to_value(mr_config)?;