use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SevctlError {
#[error(
"sevctl not found in PATH. Install AMD's sevctl (https://github.com/virtee/sevctl) and ensure it is executable."
)]
NotFound,
#[error("sevctl {command} failed (exit code {code}):\n{stderr}")]
NonZeroExit {
command: &'static str,
code: i32,
stderr: String,
},
#[error("failed to invoke sevctl: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct SessionFiles {
pub godh: PathBuf,
pub session: PathBuf,
pub tek: PathBuf,
pub tik: PathBuf,
}
#[derive(Debug, Clone)]
pub struct Sevctl {
pub(crate) path: PathBuf,
}
impl Sevctl {
pub fn find() -> Result<Self, SevctlError> {
which::which("sevctl")
.map(|path| Self { path })
.map_err(|_| SevctlError::NotFound)
}
pub async fn verify(&self, cert_path: &Path) -> Result<(), SevctlError> {
let output = tokio::process::Command::new(&self.path)
.arg("verify")
.arg("--sev")
.arg(cert_path)
.output()
.await?;
if output.status.success() {
Ok(())
} else {
let code = output.status.code().unwrap_or(-1);
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
Err(SevctlError::NonZeroExit {
command: "verify",
code,
stderr,
})
}
}
pub async fn session(
&self,
prefix: &Path,
cert_path: &Path,
policy: u32,
) -> Result<SessionFiles, SevctlError> {
let output = tokio::process::Command::new(&self.path)
.arg("session")
.arg("--name")
.arg(prefix)
.arg(cert_path)
.arg(policy.to_string())
.output()
.await?;
if !output.status.success() {
let code = output.status.code().unwrap_or(-1);
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
return Err(SevctlError::NonZeroExit {
command: "session",
code,
stderr,
});
}
let prefix_str = prefix.display().to_string();
Ok(SessionFiles {
godh: PathBuf::from(format!("{prefix_str}_godh.b64")),
session: PathBuf::from(format!("{prefix_str}_session.b64")),
tek: PathBuf::from(format!("{prefix_str}_tek.bin")),
tik: PathBuf::from(format!("{prefix_str}_tik.bin")),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
async fn retry_text_file_busy<F, Fut, T>(mut op: F) -> Result<T, SevctlError>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<T, SevctlError>>,
{
use std::io::ErrorKind;
for _ in 0..20 {
match op().await {
Err(SevctlError::Io(e)) if e.kind() == ErrorKind::ExecutableFileBusy => {
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
result => return result,
}
}
op().await
}
#[test]
fn find_reports_not_found_when_path_is_empty() {
let prev = std::env::var_os("PATH");
unsafe { std::env::set_var("PATH", "") };
let result = Sevctl::find();
if let Some(prev) = prev {
unsafe { std::env::set_var("PATH", prev) };
} else {
unsafe { std::env::remove_var("PATH") };
}
assert!(matches!(result, Err(SevctlError::NotFound)));
}
#[cfg(unix)]
#[tokio::test]
async fn verify_returns_ok_when_binary_exits_zero() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let fake = dir.path().join("sevctl");
let argv_log = dir.path().join("argv");
std::fs::write(
&fake,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$@\" > {}\nexit 0\n",
argv_log.display()
),
)
.unwrap();
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
let sevctl = Sevctl { path: fake };
let cert = dir.path().join("cert.pem");
std::fs::write(&cert, b"dummy").unwrap();
retry_text_file_busy(|| sevctl.verify(&cert)).await.unwrap();
let argv = std::fs::read_to_string(&argv_log).unwrap();
let args: Vec<&str> = argv.lines().collect();
assert_eq!(args[0], "verify");
assert_eq!(args[1], "--sev");
assert_eq!(args[2], cert.to_str().unwrap());
}
#[cfg(unix)]
#[tokio::test]
async fn verify_surfaces_non_zero_exit_with_stderr() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let fake = dir.path().join("sevctl");
std::fs::write(&fake, "#!/bin/sh\necho 'chain invalid' >&2\nexit 2\n").unwrap();
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
let sevctl = Sevctl { path: fake };
let cert = dir.path().join("cert.pem");
std::fs::write(&cert, b"dummy").unwrap();
let err = retry_text_file_busy(|| sevctl.verify(&cert))
.await
.unwrap_err();
let SevctlError::NonZeroExit {
code,
stderr,
command,
} = err
else {
panic!("expected NonZeroExit");
};
assert_eq!(code, 2);
assert_eq!(command, "verify");
assert!(stderr.contains("chain invalid"));
}
#[cfg(unix)]
#[tokio::test]
async fn session_returns_four_output_paths_and_writes_files() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let fake = dir.path().join("sevctl");
std::fs::write(
&fake,
"#!/bin/sh\nprefix=$3\necho godh > ${prefix}_godh.b64\necho session > ${prefix}_session.b64\nprintf 'tek-bytes' > ${prefix}_tek.bin\nprintf 'tik-bytes' > ${prefix}_tik.bin\nexit 0\n",
)
.unwrap();
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
let sevctl = Sevctl { path: fake };
let cert = dir.path().join("cert.pem");
std::fs::write(&cert, b"dummy").unwrap();
let prefix = dir.path().join("vm");
let files = retry_text_file_busy(|| sevctl.session(&prefix, &cert, 1))
.await
.unwrap();
assert!(files.godh.exists());
assert!(files.session.exists());
assert!(files.tek.exists());
assert!(files.tik.exists());
assert_eq!(std::fs::read(&files.tek).unwrap(), b"tek-bytes");
assert_eq!(std::fs::read(&files.tik).unwrap(), b"tik-bytes");
}
#[cfg(unix)]
#[tokio::test]
async fn session_surfaces_non_zero_exit() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let fake = dir.path().join("sevctl");
std::fs::write(
&fake,
"#!/bin/sh\necho 'session derivation failed' >&2\nexit 3\n",
)
.unwrap();
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
let sevctl = Sevctl { path: fake };
let cert = dir.path().join("cert.pem");
std::fs::write(&cert, b"dummy").unwrap();
let prefix = dir.path().join("vm");
let err = retry_text_file_busy(|| sevctl.session(&prefix, &cert, 1))
.await
.unwrap_err();
let SevctlError::NonZeroExit { code, command, .. } = err else {
panic!("expected NonZeroExit");
};
assert_eq!(code, 3);
assert_eq!(command, "session");
}
}