Skip to content

Commit 7bf167e

Browse files
committed
feat(attestation): measure QEMU swtpm device
Signed-off-by: Kevin Wang <wy721@qq.com>
1 parent f2ce587 commit 7bf167e

16 files changed

Lines changed: 271 additions & 35 deletions

File tree

docs/tutorials/attestation-verification.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ cat /var/lib/dstack/images/dstack-0.5.7/metadata.json | jq .
254254
# Install QEMU build dependencies
255255
sudo apt-get update
256256
sudo apt-get install -y git libslirp-dev python3-pip ninja-build \
257-
pkg-config libglib2.0-dev build-essential flex bison
257+
pkg-config libglib2.0-dev build-essential flex bison swtpm
258258

259259
# Clone the custom QEMU fork
260260
cd ~/dstack

docs/tutorials/kms-build-configuration.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,10 @@ FROM dstacktee/dstack-kms@sha256:11ac59f524a22462ccd2152219b0bec48a28ceb734e3250
378378
FROM ubuntu:24.04
379379
380380
# Install runtime dependencies
381-
# libglib2.0-0t64, libpixman-1-0, and libslirp0 are required by dstack-acpi-tables (QEMU binary)
381+
# The QEMU libraries are required by dstack-acpi-tables. swtpm is used when
382+
# reconstructing ACPI tables for VMs that attached a QEMU TPM device.
382383
RUN apt-get update && \
383-
apt-get install -y ca-certificates curl libglib2.0-0t64 libpixman-1-0 libslirp0 && \
384+
apt-get install -y ca-certificates curl libglib2.0-0t64 libpixman-1-0 libslirp0 swtpm && \
384385
rm -rf /var/lib/apt/lists/*
385386
386387
# Install Node.js 20.x for auth-eth

dstack/dstack-mr/cli/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ struct MachineConfig {
7474
#[arg(long, default_value = "0")]
7575
num_verity_volumes: u32,
7676

77+
/// Attach a QEMU tpm-tis device backed by swtpm
78+
#[arg(long, default_value = "false")]
79+
qemu_swtpm: bool,
80+
7781
/// Disable hotplug
7882
#[arg(long, default_value = "false")]
7983
hotplug_off: Bool,
@@ -143,6 +147,7 @@ fn main() -> Result<()> {
143147
.num_nvswitches(config.num_nvswitches)
144148
.num_nics(config.num_nics)
145149
.num_verity_volumes(config.num_verity_volumes)
150+
.qemu_swtpm(config.qemu_swtpm)
146151
.hotplug_off(config.hotplug_off)
147152
.root_verity(config.root_verity)
148153
.maybe_qemu_version(config.qemu_version.clone())

dstack/dstack-mr/src/acpi.rs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,70 @@
88
use anyhow::{bail, Context, Result};
99
use log::debug;
1010
use scale::Decode;
11+
use std::os::unix::fs::FileTypeExt;
12+
use std::path::PathBuf;
13+
use std::process::{Child, Command, Stdio};
14+
use std::sync::atomic::{AtomicU64, Ordering};
15+
use std::thread;
16+
use std::time::Duration;
1117

1218
use crate::Machine;
1319

1420
const LDR_LENGTH: usize = 4096;
1521
const FIXED_STRING_LEN: usize = 56;
22+
static SWTPM_ID: AtomicU64 = AtomicU64::new(0);
23+
24+
struct SwtpmGuard {
25+
child: Child,
26+
state_dir: PathBuf,
27+
socket: PathBuf,
28+
}
29+
30+
impl Drop for SwtpmGuard {
31+
fn drop(&mut self) {
32+
let _ = self.child.kill();
33+
let _ = self.child.wait();
34+
let _ = fs::remove_dir_all(&self.state_dir);
35+
}
36+
}
37+
38+
fn start_measurement_swtpm() -> Result<SwtpmGuard> {
39+
let id = SWTPM_ID.fetch_add(1, Ordering::Relaxed);
40+
let state_dir =
41+
std::env::temp_dir().join(format!("dstack-mr-swtpm-{}-{id}", std::process::id()));
42+
fs::create_dir_all(&state_dir).context("failed to create measurement swtpm state directory")?;
43+
let socket = state_dir.join("swtpm.sock");
44+
let child = Command::new("swtpm")
45+
.args(["socket", "--tpm2", "--tpmstate"])
46+
.arg(format!("dir={}", state_dir.display()))
47+
.arg("--ctrl")
48+
.arg(format!("type=unixio,path={}", socket.display()))
49+
.args(["--flags", "startup-clear"])
50+
.stdin(Stdio::null())
51+
.stdout(Stdio::null())
52+
.stderr(Stdio::null())
53+
.spawn()
54+
.inspect_err(|_| {
55+
let _ = fs::remove_dir_all(&state_dir);
56+
})
57+
.context("failed to start swtpm for ACPI measurement")?;
58+
let mut guard = SwtpmGuard {
59+
child,
60+
state_dir,
61+
socket,
62+
};
63+
64+
for _ in 0..100 {
65+
if fs::metadata(&guard.socket).is_ok_and(|metadata| metadata.file_type().is_socket()) {
66+
return Ok(guard);
67+
}
68+
if guard.child.try_wait()?.is_some() {
69+
bail!("measurement swtpm exited before its socket was ready");
70+
}
71+
thread::sleep(Duration::from_millis(10));
72+
}
73+
bail!("timed out waiting for measurement swtpm socket")
74+
}
1675

1776
#[derive(Debug, Clone)]
1877
pub struct Tables {
@@ -33,7 +92,7 @@ impl Machine<'_> {
3392
let shared_dir = "/bin";
3493

3594
// Prepare the command arguments
36-
let mut cmd = std::process::Command::new("dstack-acpi-tables");
95+
let mut cmd = Command::new("dstack-acpi-tables");
3796
cmd.args([
3897
"-cpu",
3998
"qemu64",
@@ -76,6 +135,18 @@ impl Machine<'_> {
76135
.arg(format!("virtio-net-pci,netdev=net{i}"));
77136
}
78137

138+
let swtpm = self.qemu_swtpm.then(start_measurement_swtpm).transpose()?;
139+
if let Some(swtpm) = &swtpm {
140+
cmd.arg("-chardev")
141+
.arg(format!("socket,id=chrtpm,path={}", swtpm.socket.display()))
142+
.args([
143+
"-tpmdev",
144+
"emulator,id=tpm0,chardev=chrtpm",
145+
"-device",
146+
"tpm-tis,tpmdev=tpm0",
147+
]);
148+
}
149+
79150
cmd.args([
80151
"-object",
81152
"tdx-guest,id=tdx",

dstack/dstack-mr/src/machine.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ pub struct Machine<'a> {
3636
/// Number of virtio-blk verity volumes attached before the NICs.
3737
#[builder(default)]
3838
pub num_verity_volumes: u32,
39+
/// Whether QEMU attaches a tpm-tis device backed by swtpm.
40+
#[builder(default)]
41+
pub qemu_swtpm: bool,
3942
pub hotplug_off: bool,
4043
pub root_verity: bool,
4144
#[builder(default)]

dstack/dstack-mr/src/tdx.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ fn select_mrtd(measurement: &TdxOsImageMeasurement, vm_config: &VmConfig) -> Res
8484
.num_gpus(vm_config.num_gpus)
8585
.num_nics(vm_config.num_nics)
8686
.num_verity_volumes(vm_config.num_verity_volumes)
87+
.qemu_swtpm(vm_config.qemu_swtpm)
8788
.num_nvswitches(vm_config.num_nvswitches)
8889
.host_share_mode(vm_config.host_share_mode.clone())
8990
.ovmf_variant(measurement.tdvf.ovmf_variant)
@@ -476,6 +477,7 @@ pub fn tdx_measurements_for_image_dir_without_rtmr0(
476477
.num_gpus(vm_config.num_gpus)
477478
.num_nics(vm_config.num_nics)
478479
.num_verity_volumes(vm_config.num_verity_volumes)
480+
.qemu_swtpm(vm_config.qemu_swtpm)
479481
.num_nvswitches(vm_config.num_nvswitches)
480482
.host_share_mode(vm_config.host_share_mode.clone())
481483
.ovmf_variant(ovmf_variant)
@@ -573,6 +575,7 @@ pub fn tdx_measurements_for_image_dir_with_acpi_hashes(
573575
.num_gpus(vm_config.num_gpus)
574576
.num_nics(vm_config.num_nics)
575577
.num_verity_volumes(vm_config.num_verity_volumes)
578+
.qemu_swtpm(vm_config.qemu_swtpm)
576579
.num_nvswitches(vm_config.num_nvswitches)
577580
.host_share_mode(vm_config.host_share_mode.clone())
578581
.ovmf_variant(ovmf_variant)

dstack/dstack-types/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,10 @@ fn is_zero(n: &u32) -> bool {
10331033
*n == 0
10341034
}
10351035

1036+
fn is_false(value: &bool) -> bool {
1037+
!value
1038+
}
1039+
10361040
#[derive(Deserialize, Serialize, Debug, Clone)]
10371041
pub struct VmConfig {
10381042
#[serde(with = "hex_bytes", default)]
@@ -1071,6 +1075,10 @@ pub struct VmConfig {
10711075
/// changes the measured ACPI/DSDT layout.
10721076
#[serde(default, skip_serializing_if = "is_zero")]
10731077
pub num_verity_volumes: u32,
1078+
/// Whether QEMU attaches a software TPM device. The TPM changes the ACPI
1079+
/// table layout and must therefore be included in TDX measurement inputs.
1080+
#[serde(default, skip_serializing_if = "is_false")]
1081+
pub qemu_swtpm: bool,
10741082
#[serde(default)]
10751083
pub hotplug_off: bool,
10761084
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -2121,4 +2129,18 @@ mod vm_config_device_count_tests {
21212129
Some(2)
21222130
);
21232131
}
2132+
2133+
#[test]
2134+
fn qemu_swtpm_is_serialized_only_when_enabled() {
2135+
let mut cfg: VmConfig = serde_json::from_value(legacy_json()).unwrap();
2136+
let serialized = serde_json::to_value(&cfg).unwrap();
2137+
assert!(serialized.get("qemu_swtpm").is_none());
2138+
2139+
cfg.qemu_swtpm = true;
2140+
let serialized = serde_json::to_value(&cfg).unwrap();
2141+
assert_eq!(
2142+
serialized.get("qemu_swtpm").and_then(|v| v.as_bool()),
2143+
Some(true)
2144+
);
2145+
}
21242146
}

dstack/kms/dstack-app/builder/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ RUN ./pin-packages.sh ./qemu-pinned-packages.txt && \
4444
python3-sphinx-rtd-theme \
4545
build-essential \
4646
flex \
47-
bison && \
47+
bison \
48+
swtpm && \
4849
rm -rf /var/lib/apt/lists/* /var/log/* /var/cache/ldconfig/aux-cache
4950
RUN git clone https://github.com/kvinwang/qemu-tdx.git --depth 1 --branch dstack-qemu-9.2.1 --single-branch && \
5051
cd qemu-tdx && git fetch --depth 1 origin ${QEMU_REV} && \

dstack/kms/dstack-app/builder/shared/qemu-pinned-packages.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ libtinfo6:amd64=6.4-4
159159
libtirpc-common=1.3.3+ds-1
160160
libtirpc-dev:amd64=1.3.3+ds-1
161161
libtirpc3:amd64=1.3.3+ds-1
162+
libtpms0:amd64=0.9.2-3.1+deb12u1
162163
libtsan2:amd64=12.2.0-14+deb12u1
163164
libubsan1:amd64=12.2.0-14+deb12u1
164165
libudev1:amd64=252.38-1~deb12u1
@@ -223,6 +224,8 @@ sed=4.9-1
223224
sgml-base=1.31
224225
sphinx-common=5.3.0-4
225226
sphinx-rtd-theme-common=1.2.0+dfsg-1
227+
swtpm-libs:amd64=0.7.1-1.3
228+
swtpm=0.7.1-1.3
226229
sysvinit-utils=3.06-4
227230
tar=1.34+dfsg-1.2+deb12u1
228231
tzdata=2025b-0+deb12u1

dstack/verifier/builder/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ RUN ./pin-packages.sh ./pinned-packages.txt && \
7676
curl \
7777
libglib2.0-0 \
7878
libslirp0 \
79+
swtpm \
7980
&& rm -rf /var/lib/apt/lists/* /var/log/* /var/cache/ldconfig/aux-cache
8081
COPY --from=verifier-builder /build/repo/dstack/target/x86_64-unknown-linux-musl/release/dstack-verifier /usr/local/bin/dstack-verifier
8182
COPY --from=verifier-builder /build/.GIT_REV /etc/

0 commit comments

Comments
 (0)