use crate::persona::OvmfVariant;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OvmfPaths {
pub code: PathBuf,
pub vars_template: PathBuf,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum OvmfError {
#[error(
"`OVMF_CODE` image not found under {firmware_root}; tried: {tried:?}. \
Install `ovmf` (Debian/Ubuntu) or `edk2-ovmf` (Fedora), or set a custom firmware_root."
)]
OvmfCodeMissing {
firmware_root: PathBuf,
tried: Vec<&'static str>,
},
#[error("`OVMF_VARS` template not found at {path}")]
VarsTemplateMissing {
path: PathBuf,
},
#[error("custom_keyring {keyring} escapes firmware_root {firmware_root}")]
CustomKeyringOutsideRoot {
keyring: PathBuf,
firmware_root: PathBuf,
},
#[error("secure_boot.ovmf_variant=custom_pk requires a custom_keyring path; none set")]
CustomKeyringRequired,
#[error("failed to canonicalize {path}: {kind}")]
Canonicalize {
path: PathBuf,
kind: String,
},
}
const DEBIAN_CODE_SECBOOT: &str = "OVMF_CODE_4M.secboot.fd";
const DEBIAN_CODE_NONSECBOOT: &str = "OVMF_CODE_4M.fd";
const DEBIAN_VARS_MS: &str = "OVMF_VARS_4M.ms.fd";
const DEBIAN_VARS_BLANK: &str = "OVMF_VARS_4M.fd";
const FEDORA_CODE_CANDIDATES: &[&str] = &[
"OVMF_CODE.secboot.4m.fd",
"OVMF_CODE.secboot.fd",
"OVMF_CODE.fd",
];
pub fn resolve(
variant: OvmfVariant,
custom_keyring: Option<&Path>,
firmware_root: &Path,
) -> Result<OvmfPaths, OvmfError> {
let want_secboot = matches!(
variant,
OvmfVariant::MsEnrolled | OvmfVariant::CustomPk | OvmfVariant::SetupMode
);
let code = find_code(firmware_root, want_secboot)?;
let vars_template = match variant {
OvmfVariant::MsEnrolled => resolve_vars(firmware_root, DEBIAN_VARS_MS)?,
OvmfVariant::SetupMode | OvmfVariant::Disabled => {
resolve_vars(firmware_root, DEBIAN_VARS_BLANK)?
}
OvmfVariant::CustomPk => {
let keyring = custom_keyring.ok_or(OvmfError::CustomKeyringRequired)?;
verify_keyring_under_root(keyring, firmware_root)?
}
};
Ok(OvmfPaths {
code,
vars_template,
})
}
fn find_code(firmware_root: &Path, want_secboot: bool) -> Result<PathBuf, OvmfError> {
let mut tried: Vec<&'static str> = Vec::new();
if want_secboot {
tried.push(DEBIAN_CODE_SECBOOT);
let p = firmware_root.join(DEBIAN_CODE_SECBOOT);
if p.exists() {
return Ok(p);
}
} else {
tried.push(DEBIAN_CODE_NONSECBOOT);
let p = firmware_root.join(DEBIAN_CODE_NONSECBOOT);
if p.exists() {
return Ok(p);
}
}
for name in FEDORA_CODE_CANDIDATES {
let has_secboot = name.contains("secboot");
if has_secboot != want_secboot {
continue;
}
tried.push(name);
let p = firmware_root.join(name);
if p.exists() {
return Ok(p);
}
}
Err(OvmfError::OvmfCodeMissing {
firmware_root: firmware_root.to_path_buf(),
tried,
})
}
fn resolve_vars(firmware_root: &Path, name: &str) -> Result<PathBuf, OvmfError> {
let p = firmware_root.join(name);
if p.exists() {
Ok(p)
} else {
Err(OvmfError::VarsTemplateMissing { path: p })
}
}
fn verify_keyring_under_root(keyring: &Path, firmware_root: &Path) -> Result<PathBuf, OvmfError> {
let root_canon = canonicalize_or_err(firmware_root)?;
let resolved = if keyring.is_absolute() {
keyring.to_path_buf()
} else {
root_canon.join(keyring)
};
let keyring_canon = canonicalize_or_err(&resolved)?;
if keyring_canon.starts_with(&root_canon) {
Ok(keyring_canon)
} else {
Err(OvmfError::CustomKeyringOutsideRoot {
keyring: keyring.to_path_buf(),
firmware_root: firmware_root.to_path_buf(),
})
}
}
fn canonicalize_or_err(path: &Path) -> Result<PathBuf, OvmfError> {
std::fs::canonicalize(path).map_err(|e| OvmfError::Canonicalize {
path: path.to_path_buf(),
kind: e.to_string(),
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn debian_firmware_root() -> (TempDir, PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().to_path_buf();
for name in [
DEBIAN_CODE_SECBOOT,
DEBIAN_CODE_NONSECBOOT,
DEBIAN_VARS_MS,
DEBIAN_VARS_BLANK,
] {
fs::write(root.join(name), b"fake firmware blob\n").unwrap();
}
(tmp, root)
}
#[test]
fn ms_enrolled_resolves_to_secboot_code_plus_ms_vars() {
let (_tmp, root) = debian_firmware_root();
let paths = resolve(OvmfVariant::MsEnrolled, None, &root).unwrap();
assert_eq!(paths.code, root.join(DEBIAN_CODE_SECBOOT));
assert_eq!(paths.vars_template, root.join(DEBIAN_VARS_MS));
}
#[test]
fn setup_mode_resolves_to_secboot_code_plus_blank_vars() {
let (_tmp, root) = debian_firmware_root();
let paths = resolve(OvmfVariant::SetupMode, None, &root).unwrap();
assert_eq!(paths.code, root.join(DEBIAN_CODE_SECBOOT));
assert_eq!(paths.vars_template, root.join(DEBIAN_VARS_BLANK));
}
#[test]
fn disabled_resolves_to_non_secboot_code_plus_blank_vars() {
let (_tmp, root) = debian_firmware_root();
let paths = resolve(OvmfVariant::Disabled, None, &root).unwrap();
assert_eq!(paths.code, root.join(DEBIAN_CODE_NONSECBOOT));
assert_eq!(paths.vars_template, root.join(DEBIAN_VARS_BLANK));
}
#[test]
fn missing_code_image_yields_named_error_with_hint() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().to_path_buf();
let err = resolve(OvmfVariant::MsEnrolled, None, &root).unwrap_err();
match err {
OvmfError::OvmfCodeMissing {
firmware_root,
tried,
} => {
assert_eq!(firmware_root, root);
assert!(
tried.contains(&DEBIAN_CODE_SECBOOT),
"should have tried Debian secboot path first"
);
}
other => panic!("expected OvmfCodeMissing, got {other:?}"),
}
}
#[test]
fn fedora_layout_resolves_when_debian_absent() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().to_path_buf();
fs::write(root.join("OVMF_CODE.secboot.4m.fd"), b"fedora fake\n").unwrap();
fs::write(root.join(DEBIAN_VARS_MS), b"ms vars fake\n").unwrap();
let paths = resolve(OvmfVariant::MsEnrolled, None, &root).unwrap();
assert_eq!(paths.code, root.join("OVMF_CODE.secboot.4m.fd"));
}
#[test]
fn missing_vars_template_yields_named_error() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().to_path_buf();
fs::write(root.join(DEBIAN_CODE_SECBOOT), b"fake\n").unwrap();
let err = resolve(OvmfVariant::MsEnrolled, None, &root).unwrap_err();
assert!(
matches!(err, OvmfError::VarsTemplateMissing { .. }),
"got {err:?}"
);
}
#[test]
fn custom_pk_without_keyring_rejected() {
let (_tmp, root) = debian_firmware_root();
let err = resolve(OvmfVariant::CustomPk, None, &root).unwrap_err();
assert_eq!(err, OvmfError::CustomKeyringRequired);
}
#[test]
fn custom_pk_with_keyring_under_root_resolves() {
let (_tmp, root) = debian_firmware_root();
let keyring = root.join("custom-pk-keyring.fd");
fs::write(&keyring, b"test keyring\n").unwrap();
let paths = resolve(OvmfVariant::CustomPk, Some(&keyring), &root).unwrap();
assert_eq!(paths.vars_template, keyring.canonicalize().unwrap());
}
#[test]
fn custom_pk_with_keyring_outside_root_rejected() {
let (_tmp, root) = debian_firmware_root();
let other = tempfile::tempdir().unwrap();
let keyring = other.path().join("escape.fd");
fs::write(&keyring, b"evil\n").unwrap();
let err = resolve(OvmfVariant::CustomPk, Some(&keyring), &root).unwrap_err();
assert!(
matches!(err, OvmfError::CustomKeyringOutsideRoot { .. }),
"got {err:?}"
);
}
#[test]
fn custom_pk_with_symlink_escape_rejected() {
let (_tmp, root) = debian_firmware_root();
let other = tempfile::tempdir().unwrap();
let target = other.path().join("real.fd");
fs::write(&target, b"outside\n").unwrap();
let symlink = root.join("looks-inside.fd");
#[cfg(unix)]
std::os::unix::fs::symlink(&target, &symlink).unwrap();
#[cfg(not(unix))]
{
let _ = symlink; return;
}
let err = resolve(OvmfVariant::CustomPk, Some(&symlink), &root).unwrap_err();
assert!(
matches!(err, OvmfError::CustomKeyringOutsideRoot { .. }),
"symlink canonicalized should escape root; got {err:?}"
);
}
#[test]
fn canonicalize_failure_surfaced_as_named_error() {
let (_tmp, root) = debian_firmware_root();
let missing = root.join("does-not-exist.fd");
let err = resolve(OvmfVariant::CustomPk, Some(&missing), &root).unwrap_err();
assert!(matches!(err, OvmfError::Canonicalize { .. }), "got {err:?}");
}
}