use serde::{Deserialize, Serialize};
use crate::backend::{Backend, SandboxBackend};
use crate::lifecycle::{ExecResult, SandboxConfig};
use crate::policy::SandboxPolicy;
#[derive(Debug)]
pub struct SevBackend {
config: SandboxConfig,
qemu_path: String,
guest_policy: SevGuestPolicy,
}
impl SevBackend {
pub fn new(config: &SandboxConfig) -> crate::Result<Self> {
let qemu_path = find_qemu().ok_or_else(|| {
crate::KavachError::BackendUnavailable("qemu-system-x86_64 not found".into())
})?;
if !std::path::Path::new("/dev/sev").exists() {
return Err(crate::KavachError::BackendUnavailable(
"SEV device /dev/sev not found".into(),
));
}
Ok(Self {
config: config.clone(),
qemu_path,
guest_policy: SevGuestPolicy::default_hardened(),
})
}
pub fn with_guest_policy(mut self, policy: SevGuestPolicy) -> Self {
self.guest_policy = policy;
self
}
fn build_qemu_args(&self, workdir: &std::path::Path, kernel_path: &str) -> Vec<String> {
let vcpus = self
.config
.policy
.cpu_limit
.map(|c| (c.ceil() as u32).max(1))
.unwrap_or(2);
let memory = self.config.policy.memory_limit_mb.unwrap_or(512);
let policy_bits = self.guest_policy.to_bits();
vec![
"-enable-kvm".into(),
"-cpu".into(),
"EPYC-v4".into(),
"-machine".into(),
"q35,confidential-guest-support=sev0,memory-backend=ram1".into(),
"-object".into(),
format!("memory-backend-memfd-private,id=ram1,size={memory}M"),
"-object".into(),
format!(
"sev-snp-guest,id=sev0,policy=0x{policy_bits:x},cbitpos={CBIT_POS},reduced-phys-bits=1"
),
"-smp".into(),
vcpus.to_string(),
"-m".into(),
format!("{memory}M"),
"-nographic".into(),
"-no-reboot".into(),
"-kernel".into(),
kernel_path.into(),
"-virtfs".into(),
format!(
"local,path={},mount_tag=task,security_model=none,readonly=on",
workdir.display()
),
]
}
pub async fn fetch_attestation(&self) -> crate::Result<SevAttestationReport> {
let output = tokio::process::Command::new("sevctl")
.args(["export", "--format", "json"])
.output()
.await
.map_err(|e| crate::KavachError::ExecFailed(format!("SEV attestation fetch: {e}")))?;
if !output.status.success() {
return Err(crate::KavachError::ExecFailed(
"sevctl attestation report retrieval failed".into(),
));
}
let report: SevAttestationReport = serde_json::from_slice(&output.stdout).map_err(|e| {
crate::KavachError::ExecFailed(format!("parse attestation report: {e}"))
})?;
tracing::debug!(
measurement = %report.measurement,
vmpl = report.vmpl,
"fetched SEV-SNP attestation report"
);
Ok(report)
}
}
const DEFAULT_KERNEL_PATH: &str = "/var/lib/kavach/vmlinuz-sev";
pub const CBIT_POS: u32 = 51;
pub const SEV_SNP_POLICY: u32 = 0x30000;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SevGuestPolicy {
pub smt_allowed: bool,
pub migration_agent_allowed: bool,
pub debug_allowed: bool,
pub single_socket_only: bool,
pub min_abi_major: u8,
pub min_abi_minor: u8,
}
impl SevGuestPolicy {
#[must_use]
pub fn default_hardened() -> Self {
Self {
smt_allowed: false,
migration_agent_allowed: false,
debug_allowed: false,
single_socket_only: false,
min_abi_major: 3,
min_abi_minor: 0,
}
}
#[must_use]
pub fn to_bits(&self) -> u64 {
let mut bits: u64 = 0;
bits |= u64::from(self.min_abi_minor) & 0xFF;
bits |= (u64::from(self.min_abi_major) & 0xFF) << 8;
if self.smt_allowed {
bits |= 1 << 16;
}
bits |= 1 << 17;
if self.migration_agent_allowed {
bits |= 1 << 18;
}
if self.debug_allowed {
bits |= 1 << 19;
}
if self.single_socket_only {
bits |= 1 << 20;
}
bits
}
#[must_use]
pub fn from_bits(bits: u64) -> Self {
Self {
min_abi_minor: (bits & 0xFF) as u8,
min_abi_major: ((bits >> 8) & 0xFF) as u8,
smt_allowed: bits & (1 << 16) != 0,
migration_agent_allowed: bits & (1 << 18) != 0,
debug_allowed: bits & (1 << 19) != 0,
single_socket_only: bits & (1 << 20) != 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SevAttestationReport {
pub report_version: u32,
pub guest_svn: u32,
pub policy: u64,
pub measurement: String,
pub host_data: String,
pub id_key_digest: String,
pub report_id: String,
pub vmpl: u32,
pub signature: Vec<u8>,
}
impl SevAttestationReport {
#[must_use]
pub fn verify(&self) -> bool {
if !is_valid_hex(&self.measurement, 96) {
tracing::warn!(len = self.measurement.len(), "invalid measurement length");
return false;
}
if !is_valid_hex(&self.report_id, 64) {
tracing::warn!(len = self.report_id.len(), "invalid report_id");
return false;
}
if self.signature.len() < 96 {
tracing::warn!(len = self.signature.len(), "signature too short");
return false;
}
if self.vmpl != 0 {
tracing::warn!(vmpl = self.vmpl, "non-zero VMPL");
return false;
}
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SevAttestationPolicy {
pub expected_measurement: Option<String>,
pub max_vmpl: u32,
pub min_guest_svn: u32,
pub required_policy_flags: u64,
}
impl SevAttestationPolicy {
#[must_use]
pub fn strict() -> Self {
Self {
expected_measurement: None,
max_vmpl: 0,
min_guest_svn: 0,
required_policy_flags: SevGuestPolicy::default_hardened().to_bits(),
}
}
#[must_use]
pub fn verify_against(&self, report: &SevAttestationReport) -> bool {
if !report.verify() {
return false;
}
if let Some(ref expected) = self.expected_measurement
&& report.measurement != *expected
{
tracing::warn!("measurement mismatch");
return false;
}
if report.vmpl > self.max_vmpl {
tracing::warn!(
vmpl = report.vmpl,
max = self.max_vmpl,
"VMPL exceeds policy maximum"
);
return false;
}
if report.guest_svn < self.min_guest_svn {
tracing::warn!(
svn = report.guest_svn,
min = self.min_guest_svn,
"guest SVN below policy minimum"
);
return false;
}
if report.policy & self.required_policy_flags != self.required_policy_flags {
tracing::warn!("report policy missing required flags");
return false;
}
true
}
}
#[inline]
fn is_valid_hex(s: &str, expected_len: usize) -> bool {
s.len() == expected_len && s.bytes().all(|b| b.is_ascii_hexdigit())
}
#[async_trait::async_trait]
impl SandboxBackend for SevBackend {
fn backend_type(&self) -> Backend {
Backend::Sev
}
async fn exec(&self, command: &str, policy: &SandboxPolicy) -> crate::Result<ExecResult> {
let workdir = tempfile::tempdir()
.map_err(|e| crate::KavachError::CreationFailed(format!("SEV workdir: {e}")))?;
let task_script = workdir.path().join("task.sh");
std::fs::write(&task_script, format!("#!/bin/sh\n{command}\n"))
.map_err(|e| crate::KavachError::CreationFailed(format!("write task: {e}")))?;
let _ = policy;
let args = self.build_qemu_args(workdir.path(), DEFAULT_KERNEL_PATH);
let mut cmd = tokio::process::Command::new(&self.qemu_path);
cmd.args(&args);
for (k, v) in &self.config.env {
cmd.env(k, v);
}
crate::backend::exec_util::execute_with_timeout(&mut cmd, self.config.timeout_ms, "qemu")
.await
}
async fn health_check(&self) -> crate::Result<bool> {
let output = tokio::process::Command::new(&self.qemu_path)
.arg("--version")
.output()
.await
.map_err(|e| crate::KavachError::ExecFailed(format!("SEV health: {e}")))?;
Ok(output.status.success())
}
async fn destroy(&self) -> crate::Result<()> {
Ok(())
}
}
fn find_qemu() -> Option<String> {
if crate::backend::which_exists("qemu-system-x86_64") {
return Some("qemu-system-x86_64".into());
}
for path in &[
"/usr/local/bin/qemu-system-x86_64",
"/usr/bin/qemu-system-x86_64",
] {
if std::path::Path::new(path).exists() {
return Some((*path).to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn make_valid_report() -> SevAttestationReport {
SevAttestationReport {
report_version: 2,
guest_svn: 1,
policy: SevGuestPolicy::default_hardened().to_bits(),
measurement: "a".repeat(96),
host_data: "b".repeat(64),
id_key_digest: "c".repeat(64),
report_id: "d".repeat(64),
vmpl: 0,
signature: vec![0xAB; 96],
}
}
#[test]
fn qemu_args_contain_sev_snp() {
let config = SandboxConfig::builder().backend(Backend::Sev).build();
let backend = SevBackend {
config: config.clone(),
qemu_path: "qemu-system-x86_64".into(),
guest_policy: SevGuestPolicy::default_hardened(),
};
let args = backend.build_qemu_args(std::path::Path::new("/tmp"), "/vmlinuz");
let joined = args.join(" ");
assert!(joined.contains("sev-snp-guest"));
assert!(joined.contains("confidential-guest-support"));
assert!(joined.contains("EPYC-v4"));
assert!(joined.contains("-enable-kvm"));
}
#[test]
fn qemu_args_use_guest_policy() {
let config = SandboxConfig::builder().backend(Backend::Sev).build();
let policy = SevGuestPolicy {
debug_allowed: true,
..SevGuestPolicy::default_hardened()
};
let backend = SevBackend {
config,
qemu_path: "qemu-system-x86_64".into(),
guest_policy: policy,
};
let args = backend.build_qemu_args(std::path::Path::new("/tmp"), "/vmlinuz");
let joined = args.join(" ");
let bits = policy.to_bits();
assert!(joined.contains(&format!("policy=0x{bits:x}")));
}
#[test]
fn qemu_args_memory_from_policy() {
let mut config = SandboxConfig::builder().backend(Backend::Sev).build();
config.policy.memory_limit_mb = Some(1024);
let backend = SevBackend {
config,
qemu_path: "qemu-system-x86_64".into(),
guest_policy: SevGuestPolicy::default_hardened(),
};
let args = backend.build_qemu_args(std::path::Path::new("/tmp"), "/vmlinuz");
assert!(args.contains(&"1024M".to_string()));
}
#[test]
fn qemu_args_cpu_from_policy() {
let mut config = SandboxConfig::builder().backend(Backend::Sev).build();
config.policy.cpu_limit = Some(4.0);
let backend = SevBackend {
config,
qemu_path: "qemu-system-x86_64".into(),
guest_policy: SevGuestPolicy::default_hardened(),
};
let args = backend.build_qemu_args(std::path::Path::new("/tmp"), "/vmlinuz");
assert!(args.contains(&"4".to_string()));
}
#[test]
fn sev_snp_policy_constant_compat() {
assert_eq!(SEV_SNP_POLICY, 0x30000);
assert_eq!(CBIT_POS, 51);
}
#[test]
fn guest_policy_default_hardened() {
let gp = SevGuestPolicy::default_hardened();
assert!(!gp.smt_allowed);
assert!(!gp.migration_agent_allowed);
assert!(!gp.debug_allowed);
assert!(!gp.single_socket_only);
assert_eq!(gp.min_abi_major, 3);
}
#[test]
fn guest_policy_to_bits_roundtrip() {
let gp = SevGuestPolicy {
smt_allowed: true,
migration_agent_allowed: false,
debug_allowed: true,
single_socket_only: true,
min_abi_major: 3,
min_abi_minor: 51,
};
let bits = gp.to_bits();
let back = SevGuestPolicy::from_bits(bits);
assert_eq!(gp.smt_allowed, back.smt_allowed);
assert_eq!(gp.migration_agent_allowed, back.migration_agent_allowed);
assert_eq!(gp.debug_allowed, back.debug_allowed);
assert_eq!(gp.single_socket_only, back.single_socket_only);
assert_eq!(gp.min_abi_major, back.min_abi_major);
assert_eq!(gp.min_abi_minor, back.min_abi_minor);
}
#[test]
fn guest_policy_bit_17_always_set() {
let gp = SevGuestPolicy::default_hardened();
assert_ne!(gp.to_bits() & (1 << 17), 0, "bit 17 (VLEK) must be set");
}
#[test]
fn guest_policy_debug_sets_bit_19() {
let mut gp = SevGuestPolicy::default_hardened();
let bits_nodebug = gp.to_bits();
assert_eq!(bits_nodebug & (1 << 19), 0);
gp.debug_allowed = true;
let bits_debug = gp.to_bits();
assert_ne!(bits_debug & (1 << 19), 0);
}
#[test]
fn attestation_report_verify_valid() {
let report = make_valid_report();
assert!(report.verify());
}
#[test]
fn attestation_report_bad_measurement() {
let mut report = make_valid_report();
report.measurement = "tooshort".into();
assert!(!report.verify());
}
#[test]
fn attestation_report_bad_measurement_nonhex() {
let mut report = make_valid_report();
report.measurement = "z".repeat(96);
assert!(!report.verify());
}
#[test]
fn attestation_report_bad_report_id() {
let mut report = make_valid_report();
report.report_id = "short".into();
assert!(!report.verify());
}
#[test]
fn attestation_report_short_signature() {
let mut report = make_valid_report();
report.signature = vec![0; 10];
assert!(!report.verify());
}
#[test]
fn attestation_report_nonzero_vmpl() {
let mut report = make_valid_report();
report.vmpl = 1;
assert!(!report.verify());
}
#[test]
fn attestation_policy_strict_accepts_valid() {
let report = make_valid_report();
let policy = SevAttestationPolicy::strict();
assert!(policy.verify_against(&report));
}
#[test]
fn attestation_policy_measurement_mismatch() {
let report = make_valid_report();
let policy = SevAttestationPolicy {
expected_measurement: Some("f".repeat(96)),
..SevAttestationPolicy::strict()
};
assert!(!policy.verify_against(&report));
}
#[test]
fn attestation_policy_vmpl_exceeded() {
let mut report = make_valid_report();
report.vmpl = 0; let policy = SevAttestationPolicy {
max_vmpl: 0,
..SevAttestationPolicy::strict()
};
assert!(policy.verify_against(&report));
}
#[test]
fn attestation_policy_svn_too_low() {
let mut report = make_valid_report();
report.guest_svn = 0;
let policy = SevAttestationPolicy {
min_guest_svn: 5,
..SevAttestationPolicy::strict()
};
assert!(!policy.verify_against(&report));
}
#[test]
fn attestation_policy_serde_roundtrip() {
let policy = SevAttestationPolicy::strict();
let json = serde_json::to_string(&policy).unwrap();
let back: SevAttestationPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(policy.max_vmpl, back.max_vmpl);
assert_eq!(policy.min_guest_svn, back.min_guest_svn);
}
#[test]
fn attestation_report_serde_roundtrip() {
let report = make_valid_report();
let json = serde_json::to_string(&report).unwrap();
let back: SevAttestationReport = serde_json::from_str(&json).unwrap();
assert_eq!(report.measurement, back.measurement);
assert_eq!(report.vmpl, back.vmpl);
assert_eq!(report.signature, back.signature);
}
#[test]
fn is_valid_hex_works() {
assert!(is_valid_hex("abcdef0123456789", 16));
assert!(is_valid_hex("ABCDEF0123456789", 16));
assert!(!is_valid_hex("xyz", 3));
assert!(!is_valid_hex("abcdef", 16));
}
#[test]
fn find_qemu_does_not_panic() {
let _ = find_qemu();
}
}