Skip to content
Open
3 changes: 3 additions & 0 deletions dstack/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

151 changes: 109 additions & 42 deletions dstack/crates/mock-attestation/src/tdx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,73 @@
// 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,
];

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<u8>,
}

struct DeterministicP256KeyPair {
key: SigningKey,
public_key: Vec<u8>,
}

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<Vec<u8>, 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<u8>,
pub collateral: QuoteCollateralV3,
Expand All @@ -47,10 +81,13 @@ impl TdxGenerator {
}

pub fn from_seed(seed: [u8; 32]) -> Result<Self> {
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",
Expand All @@ -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(),
Expand All @@ -89,7 +125,7 @@ impl TdxGenerator {
.to_vec();
Ok(Self {
root,
root_key,
root_signing_key,
pck,
pck_key,
tcb_signer,
Expand All @@ -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<QuoteCollateralV3> {
Expand Down Expand Up @@ -166,8 +205,7 @@ impl TdxGenerator {
.encode()
.try_into()
.map_err(|bytes: Vec<u8>| 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 {
Expand Down Expand Up @@ -263,8 +301,16 @@ impl TdxGenerator {
}
}

fn make_root(seed: &[u8; 32]) -> Result<CertifiedKey> {
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([
Expand All @@ -273,7 +319,7 @@ fn make_root(seed: &[u8; 32]) -> Result<CertifiedKey> {
KeyUsagePurpose::CrlSign,
]);
let cert = params.self_signed(&key_pair)?;
Ok(CertifiedKey { cert, key_pair })
Ok((CertifiedKey { cert, key_pair }, signing_key))
}

fn make_leaf(
Expand All @@ -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
Expand All @@ -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<CertificateParams> {
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<OffsetDateTime> {
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));
Expand Down Expand Up @@ -347,18 +396,35 @@ fn pck_extension() -> CustomExtension {
CustomExtension::from_oid_content(&[1, 2, 840, 113741, 1, 13, 1], der)
}

fn signing_key(key: &KeyPair) -> Result<SigningKey> {
Ok(SigningKey::from_pkcs8_pem(&key.serialize_pem())?)
}
fn sign_raw(key: &KeyPair, message: &[u8]) -> Result<Vec<u8>> {
let sig: Signature = signing_key(key)?.sign(message);
fn sign_raw(key: &SigningKey, message: &[u8]) -> Result<Vec<u8>> {
let sig: Signature = key.sign(message);
Ok(sig.to_bytes().to_vec())
}

#[cfg(test)]
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();
Expand All @@ -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()
);
}
}
20 changes: 19 additions & 1 deletion dstack/dstack-attest/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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;
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions dstack/dstack-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,12 @@ pub struct SysConfig {
/// for fields that are absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collateral_urls: Option<CollateralUrls>,
/// 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<String>,
/// 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")]
Expand Down Expand Up @@ -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<String>,
/// Exact public PEM root shared with verifiers of simulated TDX evidence.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tdx_root_ca: Option<String>,
/// MrConfigV3 document used to generate mock platform evidence.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mr_config: Option<String>,
Expand Down
23 changes: 19 additions & 4 deletions dstack/dstack-util/src/system_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<AttestationVerifier>> {
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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions dstack/guest-agent-simulator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Loading
Loading