Skip to content
Draft
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
2 changes: 1 addition & 1 deletion configs/qemu-target.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"kernel": {
"svsm": {
"features": "vtpm",
"features": "vtpm,virtio-drivers,vsock",
"binary": false
},
"bldr": {
Expand Down
20 changes: 20 additions & 0 deletions kernel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,26 @@ virtio-drivers = ["dep:virtio-drivers"]
block = []
vsock = []

# Persistent vTPM via TPM-sealing of internal state to an off-CVM TPM
# endpoint (research extension; mainline behaviour preserved when off).
#
# When OFF (default): the `vtpm` feature provides the standard ephemeral
# vTPM that the mainline COCONUT-SVSM tree exposes — each cold boot
# re-manufactures the simulator and produces a fresh EK/SRK/IAK.
#
# When ON: svsm.rs drives the Provision/Recover seal cycle through a
# `VsockTransport`/`VsockHostStore` pair. The host helper (TPM endpoint
# on a VSOCK port + blob store on two VSOCK ports) must be reachable;
# unreachable endpoints are a fatal error, not a silent fallback,
# because silent fallback would weaken the security model that treats
# the physical TPM as a separate root of trust from the platform.
#
# Transport / store backends are currently fixed to VSOCK because that
# is the only supported channel between an SEV-SNP CVM and a host-side
# TPM endpoint; the `TpmTransport` and `SealedBlobStore` traits keep
# the door open for future block-layer or KBS backends.
vtpm-persist = ["vtpm", "vsock"]

[dev-dependencies]
sha2 = { workspace = true, features = ["force-soft"] }

Expand Down
6 changes: 3 additions & 3 deletions kernel/src/cpu/x86/apic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ impl X86Apic {
/// - `icr` - Value to write to the ICR register
#[inline(always)]
pub fn icr_write(&self, icr: u64) {
self.regs()
.icr_write(icr)
.expect("Failed to write APIC.ICR");
if let Err(e) = self.regs().icr_write(icr) {
log::warn!("Failed to write APIC.ICR: {e:?}");
}
}

/// Checks whether an IRQ vector is currently in service
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub mod virtio;
pub mod vmm;
#[cfg(feature = "vsock")]
pub mod vsock;
#[cfg(all(feature = "vtpm", not(test)))]
#[cfg(feature = "vtpm")]
pub mod vtpm;

#[test]
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/protocols/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub mod apic;
pub mod attest;
pub mod core;
pub mod errors;
#[cfg(all(feature = "vtpm", not(test)))]
#[cfg(feature = "vtpm")]
pub mod vtpm;

extern crate alloc;
Expand Down
19 changes: 11 additions & 8 deletions kernel/src/sev/snp_apic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,18 @@ impl ApicAccess for GHCBApicAccessor {
}

fn icr_write(&self, icr: u64) -> Result<(), SvsmError> {
// The #HV IPI can only be used if restricted injection is supported.
// Otherwise, the IPI must be sent via an X2APIC ICR write.
if self.use_restr_inj() {
current_ghcb().hv_ipi(icr)?;
} else {
self.apic_write(APIC_OFFSET_ICR, icr);
// HV_IPI (GHCB exit 0x0015) is a base SEV-ES protocol feature and
// does not require restricted injection. KVM rejects direct WRMSR to
// the x2APIC ICR MSR (0x830) via the GHCB MSR protocol (error 2),
// so HV_IPI is the only reliable path.
match current_ghcb().hv_ipi(icr) {
Ok(()) => Ok(()),
Err(_) => {
// Fallback: try direct MSR write if HV_IPI is unsupported
self.apic_write(APIC_OFFSET_ICR, icr);
Ok(())
}
}

Ok(())
}

fn eoi(&self) {
Expand Down
72 changes: 69 additions & 3 deletions kernel/src/svsm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,20 @@ use svsm::utils::ScopedMut;
use svsm::utils::round_to_pages;
#[cfg(all(feature = "virtio-drivers", any(feature = "block", feature = "vsock")))]
use svsm::virtio::probe_mmio_slots;
#[cfg(all(feature = "vtpm", not(test)))]
#[cfg(all(feature = "vtpm-persist", not(feature = "vsock"), not(test)))]
use svsm::vtpm::MockTransport;
#[cfg(all(feature = "vtpm-persist", feature = "vsock", not(test)))]
use svsm::vtpm::VsockTransport;
#[cfg(all(feature = "vtpm", not(feature = "vtpm-persist"), not(test)))]
use svsm::vtpm::vtpm_init;
#[cfg(all(feature = "vtpm-persist", not(test)))]
use svsm::vtpm::{SealedBlobStore, VsockHostStore, VtpmBootMode, vtpm_init_sealed};
#[cfg(all(feature = "vtpm-persist", feature = "vsock", not(test)))]
use svsm::vtpm::{VSOCK_HOST_CID, VSOCK_TPM_PORT};

use alloc::string::String;
#[cfg(all(feature = "vtpm-persist", not(test)))]
use alloc::vec::Vec;
use release::COCONUT_VERSION;

#[cfg(feature = "attest")]
Expand Down Expand Up @@ -570,8 +580,64 @@ fn svsm_init(launch_info: &KernelLaunchInfo) {
log::info!("attestation successful");
}

#[cfg(all(feature = "vtpm", not(test)))]
vtpm_init().expect("vTPM failed to initialize");
// Mainline COCONUT-SVSM behaviour: ephemeral vTPM, no external
// dependencies, fresh seeds on every cold boot. This is the path
// upstream consumers get and the one preserved across the
// `vtpm-persist` feature being off.
#[cfg(all(feature = "vtpm", not(feature = "vtpm-persist"), not(test)))]
{
vtpm_init().expect("vtpm_init failed");
log::info!("VTPM: ephemeral init complete");
}

// Persistent vTPM path — opt-in via the `vtpm-persist` feature.
// Drives the Provision/Recover seal cycle against an off-CVM TPM
// endpoint (transport) and host-side blob store, both currently
// reached over VSOCK. The host helper must be reachable: a
// missing/unreachable endpoint is a hard error, NOT a silent
// fallback to ephemeral — silently downgrading would void the
// security model the seal cycle promises, which treats the
// physical TPM as a separate root of trust (an attacker who can
// break the VSOCK channel would otherwise harvest a fresh vTPM
// identity by triggering the fallback).
#[cfg(all(feature = "vtpm-persist", not(test)))]
{
let store = VsockHostStore::new(
VSOCK_HOST_CID,
VsockHostStore::DEFAULT_LOAD_PORT,
VsockHostStore::DEFAULT_SAVE_PORT,
);

let blob_opt = store
.load()
.expect("VTPM: sealed-store load failed (host helper unreachable?)");
let (boot_mode, blob_slice) = match blob_opt.as_deref() {
Some(b) => (VtpmBootMode::Recover, Some(b)),
None => (VtpmBootMode::Provision, None),
};

let vm_id: [u8; 16] = [0u8; 16]; // TODO: derive from IGVM guest_context

let transport = VsockTransport::new(VSOCK_HOST_CID, VSOCK_TPM_PORT);

// R1: vtpm_init_sealed now uses an out-param for the Vec<u8>; its
// return type is the register-passable `Result<(), SvsmReqError>`.
let mut maybe_blob: Option<Vec<u8>> = None;
vtpm_init_sealed(transport, boot_mode, vm_id, blob_slice, &mut maybe_blob)
.expect("VTPM: sealed init failed (host TPM endpoint unreachable?)");

if let Some(blob_bytes) = maybe_blob {
store
.save(&blob_bytes)
.expect("VTPM: sealed-store save failed");
log::info!(
"VTPM: SealedBlob ({} bytes) stored for next boot",
blob_bytes.len()
);
}

log::info!("VTPM: sealed init complete (mode={boot_mode:?})");
}

virt_log_usage();

Expand Down
Loading