use crate::persona::{LockdownMode, 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=kexec-unsigned",
"aegis-boot-test: kexec-unsigned starting",
"aegis-boot-test: kexec-unsigned REJECTED",
];
pub struct KexecRefusesUnsigned;
impl Scenario for KexecRefusesUnsigned {
fn name(&self) -> &'static str {
"kexec-refuses-unsigned"
}
fn description(&self) -> &'static str {
"boot OVMF + persona + signed stick under enforcing SB + kernel \
lockdown; trigger rescue-tui's kexec-test of an unsigned kernel; \
assert kernel rejects with EKEYREJECTED + rescue-tui surfaces \
its diagnostic"
}
fn run(&self, ctx: &ScenarioContext) -> Result<ScenarioResult, ScenarioError> {
if let Some(skip) = check_skip_gates(ctx) {
return Ok(skip);
}
let swtpm_spec = SwtpmSpec::derive("kexec-test", &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 — kexec test can't run. \
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=kexec-unsigned` did not fire. \
The stick's grub.cfg needs `aegis.test=kexec-unsigned` 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!(
"test landmark '{landmark}' not seen within {}s after \
the cmdline-driven test mode entered. Either the kernel \
UNEXPECTEDLY-LOADED an unsigned blob (signed-chain regression) \
or rescue-tui's diagnostic format drifted (see aegis-boot \
docs/rescue-tui-serial-format.md substring contract). \
Serial log: {}.",
LANDMARK_TIMEOUT.as_secs(),
log_path.display(),
),
});
}
}
Ok(ScenarioResult::Pass)
}
}
fn check_skip_gates(ctx: &ScenarioContext) -> Option<ScenarioResult> {
if !ctx.stick.is_file() {
return Some(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 Some(ScenarioResult::Skip {
reason: "qemu-system-x86_64 not on PATH (Debian: apt install qemu-system-x86)"
.to_string(),
});
}
match ctx.persona.secure_boot.ovmf_variant {
OvmfVariant::Disabled | OvmfVariant::SetupMode => {
return Some(ScenarioResult::Skip {
reason: format!(
"persona {} has ovmf_variant={:?}; kexec-rejection requires \
enforcing Secure Boot (ms_enrolled or custom_pk)",
ctx.persona.id, ctx.persona.secure_boot.ovmf_variant
),
});
}
OvmfVariant::MsEnrolled | OvmfVariant::CustomPk => {}
}
match ctx.persona.kernel.lockdown {
LockdownMode::None | LockdownMode::Inherit => {
return Some(ScenarioResult::Skip {
reason: format!(
"persona {} has kernel.lockdown={:?}; kexec-rejection requires \
explicit lockdown=integrity or =confidentiality",
ctx.persona.id, ctx.persona.kernel.lockdown
),
});
}
LockdownMode::Integrity | LockdownMode::Confidentiality => {}
}
let needs_tpm = !matches!(ctx.persona.tpm.version, crate::persona::TpmVersion::None);
if needs_tpm && !binary_on_path("swtpm") {
return Some(ScenarioResult::Skip {
reason: "swtpm not on PATH (Debian: apt install swtpm); \
persona requires TPM emulation"
.to_string(),
});
}
None
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::persona::Persona;
use std::path::PathBuf;
fn enforcing_persona_yaml() -> &'static str {
r"
schema_version: 1
id: test-enforcing
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
kernel:
lockdown: integrity
"
}
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 = KexecRefusesUnsigned;
assert_eq!(s.name(), "kexec-refuses-unsigned");
assert!(s.description().contains("EKEYREJECTED"));
assert!(s.description().contains("rescue-tui"));
}
#[test]
fn skips_when_stick_missing() {
let s = KexecRefusesUnsigned;
let result = s
.run(&fake_ctx(
make_persona(enforcing_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_secure_boot_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(enforcing_persona_yaml());
p.secure_boot.ovmf_variant = OvmfVariant::Disabled;
let s = KexecRefusesUnsigned;
let result = s.run(&fake_ctx(p, stick)).unwrap();
match result {
ScenarioResult::Skip { reason } => {
assert!(
reason.contains("ovmf_variant"),
"expected ovmf_variant in skip reason: {reason}"
);
assert!(
reason.contains("enforcing"),
"expected 'enforcing' in 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(enforcing_persona_yaml());
p.secure_boot.ovmf_variant = OvmfVariant::SetupMode;
let s = KexecRefusesUnsigned;
let result = s.run(&fake_ctx(p, stick)).unwrap();
assert!(matches!(result, ScenarioResult::Skip { .. }));
}
#[test]
fn skips_when_lockdown_none() {
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(enforcing_persona_yaml());
p.kernel.lockdown = LockdownMode::None;
let s = KexecRefusesUnsigned;
let result = s.run(&fake_ctx(p, stick)).unwrap();
match result {
ScenarioResult::Skip { reason } => {
assert!(
reason.contains("lockdown"),
"expected 'lockdown' in skip reason: {reason}"
);
}
other => panic!("expected Skip, got {other:?}"),
}
}
#[test]
fn skips_when_lockdown_inherit() {
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(enforcing_persona_yaml());
p.kernel.lockdown = LockdownMode::Inherit;
let s = KexecRefusesUnsigned;
let result = s.run(&fake_ctx(p, stick)).unwrap();
assert!(matches!(result, ScenarioResult::Skip { .. }));
}
#[test]
fn accepts_lockdown_confidentiality() {
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(enforcing_persona_yaml());
p.kernel.lockdown = LockdownMode::Confidentiality;
let s = KexecRefusesUnsigned;
let _ctx = fake_ctx(p, stick);
assert!(s.description().contains("rescue-tui"));
}
}