Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dstack/Cargo.lock

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

15 changes: 14 additions & 1 deletion dstack/crates/mock-attestation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,22 @@ into verifier/KMS/gateway. The independently running host collateral service
reconstructs the same hierarchy from the seed. Configure it under
`[attestation.urls]`: TDX uses `pccs`, and SEV-SNP uses `amd_kds`.

Every verifier process must also explicitly set
The guest needs those roots too, to verify the KMS and the gateway it talks to.
They do not travel from the host: `dstack-tee-simulator` derives them from the
same seed and writes them to `/run/dstack/attestation`, guest tmpfs the host
cannot reach, before `dstack-prepare` starts. `dstack-util` reads that one
directory and nothing else, so a host can never nominate the trust anchor that
authenticates its guest's key provider. Only the development image ships the
simulator, and image contents are measured, so on a production image the
directory never exists and vendor production roots are the only outcome.

Every service configured with a mock root through its own TOML — KMS, gateway,
`dstack-verifier` — must also explicitly set
`attestation.insecure_allow_external_trust_anchors = true`. Merely mounting and
configuring a mock root is rejected at startup while this flag remains false.
The flag exists to make an operator acknowledge a hand-written non-production
root, so it has no counterpart in the guest handoff above, where one program
writes the roots and the next reads them out of a directory it authenticates.

The seed adds only 64 hex bytes (the simulator config is well below 1 KiB).

Expand Down
132 changes: 99 additions & 33 deletions dstack/crates/mock-attestation/src/tdx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,67 @@ use dcap_qvl::quote::{
};
use dcap_qvl::QuoteCollateralV3;
use p256::ecdsa::{signature::Signer, Signature, SigningKey};
use p256::pkcs8::DecodePrivateKey;
use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey};
use rcgen::{
BasicConstraints, Certificate, CertificateParams, CertificateRevocationListParams,
CertifiedKey, CustomExtension, DnType, ExtendedKeyUsagePurpose, IsCa, KeyIdMethod, KeyPair,
KeyUsagePurpose, SerialNumber,
KeyUsagePurpose, RemoteKeyPair, SerialNumber, SignatureAlgorithm, PKCS_ECDSA_P256_SHA256,
};
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 @@ -106,8 +142,11 @@ impl TdxGenerator {
pub fn root_ca_pem(&self) -> String {
self.root.pem()
}
pub fn root_key_pem(&self) -> String {
self.root_key.serialize_pem()
pub fn root_key_pem(&self) -> Result<String> {
Ok(self
.root_signing_key
.to_pkcs8_pem(Default::default())?
.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 Down
4 changes: 3 additions & 1 deletion dstack/dstack-attest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dstack-types.workspace = true
ez-hash.workspace = true
fs-err.workspace = true
safe-write.workspace = true
rustix.workspace = true
rustix = { workspace = true, features = ["process"] }
hex.workspace = true
hex_fmt.workspace = true
or-panic.workspace = true
Expand Down Expand Up @@ -62,3 +62,5 @@ quote = [
futures = { workspace = true }
tokio = { workspace = true, features = ["full"] }
dstack-mr = { workspace = true }
rcgen = { workspace = true }
tempfile = { workspace = true }
5 changes: 4 additions & 1 deletion dstack/dstack-attest/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2801,7 +2801,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
36 changes: 36 additions & 0 deletions dstack/dstack-attest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,48 @@ pub mod attestation;
mod aws_nitro_tpm;
#[cfg(feature = "quote")]
mod sev_snp;
pub mod trust_anchors;
mod v1;

const RUNTIME_EVENT_DIR: &str = "/run/log/dstack";
const RUNTIME_EVENT_VERSION_FILE: &str = "/run/log/dstack/runtime_event_version";
const RUNTIME_EVENT_LOCK_FILE: &str = "/run/log/dstack/runtime_event.lock";

/// Build the verifier a guest authenticates the KMS and the gateway with.
///
/// Trust anchors are taken from [`trust_anchors::ANCHOR_DIR`] when that
/// directory holds a set published inside this guest. When it does not — the
/// only outcome on a production image — the vendor production roots apply.
///
/// `collateral_urls` selects where signed collateral is fetched from; the trust
/// anchor still has to sign it.
pub fn default_verifier(
collateral_urls: &attestation::CollateralUrls,
) -> anyhow::Result<attestation::AttestationVerifier> {
use attestation::{AttestationVerifier, AttestationVerifierConfig};

let Some(root_ca) =
trust_anchors::load_anchors(std::path::Path::new(trust_anchors::ANCHOR_DIR))
.context("failed to load local attestation anchors")?
else {
return AttestationVerifier::new_prod(Some(collateral_urls));
};
tracing::warn!(
dir = trust_anchors::ANCHOR_DIR,
"verifying attestation against external trust anchors published by the in-guest TEE \
simulator; this guest cannot verify production evidence"
);
AttestationVerifier::load(&AttestationVerifierConfig {
// The opt-in exists to make an operator acknowledge a non-production
// root in a hand-written service config. Nothing here is hand-written:
// the roots came from a guest-local directory `load_anchors` already
// authenticated, so the flag has no one left to warn.
insecure_allow_external_trust_anchors: true,
urls: collateral_urls.clone(),
root_ca,
})
}

/// Acquire the system-wide runtime event lock, blocking until it is available.
///
/// The wait is deliberately unbounded. The lock serializes the event-log append
Expand Down
Loading
Loading