use crate::persona::OvmfVariant;
use crate::qemu::Invocation;
use crate::scenario::{Scenario, ScenarioContext, ScenarioError, ScenarioResult};
use crate::scenarios::common::binary_on_path;
use crate::serial::SerialCapture;
use crate::swtpm::{SwtpmInstance, SwtpmSpec};
use std::time::Duration;
const LANDMARK_TIMEOUT: Duration = Duration::from_secs(60);
const PREREQ_LANDMARKS: &[&str] = &["EFI stub: UEFI Secure Boot is enabled"];
const TEST_LANDMARKS: &[&str] = &[
"init: AEGIS_TEST=mok-enroll",
"MOK enrollment walkthrough",
"STEP 1/3",
"sudo mokutil --import",
];
pub struct MokEnrollAlpine;
impl Scenario for MokEnrollAlpine {
fn name(&self) -> &'static str {
"mok-enroll-alpine"
}
fn description(&self) -> &'static str {
"boot Alpine (unsigned kernel) under MS-enrolled SB; assert aegis-boot \
rescue-tui's MOK walkthrough STEP 1/3 `sudo mokutil --import` appears \
on serial verbatim per aegis-boot#202"
}
fn run(&self, ctx: &ScenarioContext) -> Result<ScenarioResult, ScenarioError> {
if !ctx.stick.is_file() {
return Ok(ScenarioResult::Skip {
reason: format!(
"stick {} not found; provision via aegis-boot flash or set AEGIS_HWSIM_STICK",
ctx.stick.display()
),
});
}
if !binary_on_path("qemu-system-x86_64") {
return Ok(ScenarioResult::Skip {
reason: "qemu-system-x86_64 not on PATH (Debian: apt install qemu-system-x86)"
.to_string(),
});
}
if !matches!(
ctx.persona.secure_boot.ovmf_variant,
OvmfVariant::MsEnrolled
) {
return Ok(ScenarioResult::Skip {
reason: format!(
"persona {} has ovmf_variant={:?}; MOK enrollment walkthrough \
applies to ms_enrolled only",
ctx.persona.id, ctx.persona.secure_boot.ovmf_variant
),
});
}
let needs_tpm = !matches!(ctx.persona.tpm.version, crate::persona::TpmVersion::None);
if needs_tpm && !binary_on_path("swtpm") {
return Ok(ScenarioResult::Skip {
reason: "swtpm not on PATH (Debian: apt install swtpm); \
persona requires TPM emulation"
.to_string(),
});
}
let swtpm_spec = SwtpmSpec::derive("mok-enroll", &ctx.work_dir, ctx.persona.tpm.version);
let swtpm = SwtpmInstance::spawn(&swtpm_spec)?;
let inv = Invocation::new(
&ctx.persona,
&ctx.stick,
&ctx.work_dir,
&ctx.firmware_root,
&swtpm,
)?;
let log_path = ctx.work_dir.join("serial.log");
let handle = SerialCapture::spawn(inv.build(), &log_path, None)?;
for landmark in PREREQ_LANDMARKS {
if handle.wait_for_line(landmark, LANDMARK_TIMEOUT).is_none() {
return Ok(ScenarioResult::Fail {
reason: format!(
"prerequisite landmark '{landmark}' not seen within {}s. \
Boot didn't reach kernel-userspace handoff — MOK walkthrough can't fire. \
Serial log: {}.",
LANDMARK_TIMEOUT.as_secs(),
log_path.display(),
),
});
}
}
match handle.wait_for_line(TEST_LANDMARKS[0], LANDMARK_TIMEOUT) {
Some(_) => {}
None => {
return Ok(ScenarioResult::Skip {
reason: format!(
"kernel reached but `init: AEGIS_TEST=mok-enroll` did not fire. \
The stick's grub.cfg needs `aegis.test=mok-enroll` on the \
kernel cmdline (see aegis-boot scripts/build-initramfs.sh, PR #680). \
Serial log: {}.",
log_path.display()
),
});
}
}
for landmark in &TEST_LANDMARKS[1..] {
if handle.wait_for_line(landmark, LANDMARK_TIMEOUT).is_none() {
return Ok(ScenarioResult::Fail {
reason: format!(
"init detected the cmdline but '{landmark}' not seen within {}s. \
Either rescue-tui's mok-enroll dispatcher regressed, or \
aegis-boot#202's MOK walkthrough text drifted (see \
docs/rescue-tui-serial-format.md substring contract). \
Serial log: {}.",
LANDMARK_TIMEOUT.as_secs(),
log_path.display(),
),
});
}
}
Ok(ScenarioResult::Pass)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::persona::Persona;
use std::path::PathBuf;
fn ms_enrolled_persona_yaml() -> &'static str {
r"
schema_version: 1
id: test-ms
vendor: QEMU
display_name: Test
source:
kind: vendor_docs
ref_: test
dmi:
sys_vendor: QEMU
product_name: Standard PC
bios_vendor: EDK II
bios_version: stable
bios_date: 01/01/2024
secure_boot:
ovmf_variant: ms_enrolled
tpm:
version: none
"
}
fn make_persona(yaml: &str) -> Persona {
serde_yaml_ng::from_str(yaml).unwrap()
}
fn fake_ctx(persona: Persona, stick: PathBuf) -> ScenarioContext {
ScenarioContext {
persona,
stick,
work_dir: tempfile::tempdir().unwrap().path().to_path_buf(),
firmware_root: PathBuf::from("/usr/share/OVMF"),
}
}
#[test]
fn name_and_description_are_stable() {
let s = MokEnrollAlpine;
assert_eq!(s.name(), "mok-enroll-alpine");
assert!(s.description().contains("mokutil"));
assert!(s.description().contains("aegis-boot#202"));
}
#[test]
fn skips_when_stick_missing() {
let s = MokEnrollAlpine;
let result = s
.run(&fake_ctx(
make_persona(ms_enrolled_persona_yaml()),
PathBuf::from("/no/such/stick.img"),
))
.unwrap();
match result {
ScenarioResult::Skip { reason } => {
assert!(reason.contains("not found"), "got reason: {reason}");
}
other => panic!("expected Skip, got {other:?}"),
}
}
#[test]
fn skips_when_custom_pk() {
let tmp = tempfile::tempdir().unwrap();
let stick = tmp.path().join("fake-stick.img");
std::fs::write(&stick, b"placeholder").unwrap();
let mut p = make_persona(ms_enrolled_persona_yaml());
p.secure_boot.ovmf_variant = OvmfVariant::CustomPk;
let s = MokEnrollAlpine;
let result = s.run(&fake_ctx(p, stick)).unwrap();
match result {
ScenarioResult::Skip { reason } => {
assert!(
reason.contains("ms_enrolled only"),
"expected ms_enrolled-only skip reason: {reason}"
);
}
other => panic!("expected Skip, got {other:?}"),
}
}
#[test]
fn skips_when_setup_mode() {
let tmp = tempfile::tempdir().unwrap();
let stick = tmp.path().join("fake-stick.img");
std::fs::write(&stick, b"placeholder").unwrap();
let mut p = make_persona(ms_enrolled_persona_yaml());
p.secure_boot.ovmf_variant = OvmfVariant::SetupMode;
let s = MokEnrollAlpine;
let result = s.run(&fake_ctx(p, stick)).unwrap();
assert!(matches!(result, ScenarioResult::Skip { .. }));
}
#[test]
fn skips_when_disabled() {
let tmp = tempfile::tempdir().unwrap();
let stick = tmp.path().join("fake-stick.img");
std::fs::write(&stick, b"placeholder").unwrap();
let mut p = make_persona(ms_enrolled_persona_yaml());
p.secure_boot.ovmf_variant = OvmfVariant::Disabled;
let s = MokEnrollAlpine;
let result = s.run(&fake_ctx(p, stick)).unwrap();
assert!(matches!(result, ScenarioResult::Skip { .. }));
}
}