use crate::persona::{OvmfVariant, TpmVersion};
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=manifest-roundtrip",
"aegis-boot-test: manifest-roundtrip starting",
"aegis-boot-test: manifest-roundtrip parsed",
];
const FAILURE_LANDMARKS: &[&str] = &[
"aegis-boot-test: manifest-roundtrip FAILED",
" MISMATCH (",
" READ-FAILED (",
];
pub struct AttestationRoundtrip;
impl Scenario for AttestationRoundtrip {
fn name(&self) -> &'static str {
"attestation-roundtrip"
}
fn description(&self) -> &'static str {
"boot OVMF + persona + signed stick under TPM-bearing SB enforcement; trigger \
rescue-tui's manifest-roundtrip test mode (aegis-boot#697); assert manifest \
parses cleanly and PCR roundtrip matches (or empty-pcrs fail-open per \
attestation-manifest.md contract)"
}
fn run(&self, ctx: &ScenarioContext) -> Result<ScenarioResult, ScenarioError> {
if let Some(skip) = check_skip_gates(ctx) {
return Ok(skip);
}
let swtpm_spec =
SwtpmSpec::derive("manifest-roundtrip", &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. Serial log: {}.",
LANDMARK_TIMEOUT.as_secs(),
log_path.display(),
),
});
}
}
if handle
.wait_for_line(TEST_LANDMARKS[0], LANDMARK_TIMEOUT)
.is_none()
{
return Ok(ScenarioResult::Skip {
reason: format!(
"kernel reached but `init: AEGIS_TEST=manifest-roundtrip` did not fire. \
Re-flash the stick with `MKUSB_TEST_MODE=manifest-roundtrip ./scripts/mkusb.sh` \
(aegis-boot#696). 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 manifest-roundtrip dispatcher regressed, or \
the rescue-tui printed a `FAILED (...)` message before reaching the \
parsed-manifest stage (check serial log). Serial log: {}.",
LANDMARK_TIMEOUT.as_secs(),
log_path.display(),
),
});
}
}
let buffer = handle.buffer_snapshot();
for needle in FAILURE_LANDMARKS {
if buffer.contains(needle) {
return Ok(ScenarioResult::Fail {
reason: format!(
"manifest parsed but '{needle}' substring appeared in the test mode's \
output. The PCR roundtrip detected drift between the on-stick \
manifest and the measured boot, OR the test mode hit a \
post-parse failure (TPM driver issue, etc.). Serial log: {}.",
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(),
});
}
if matches!(ctx.persona.secure_boot.ovmf_variant, OvmfVariant::Disabled) {
return Some(ScenarioResult::Skip {
reason: format!(
"persona {} has ovmf_variant=disabled; manifest-roundtrip needs a \
signed-chain context to attest",
ctx.persona.id
),
});
}
if matches!(ctx.persona.tpm.version, TpmVersion::None) {
return Some(ScenarioResult::Skip {
reason: format!(
"persona {} has no TPM (tpm.version=none); manifest-roundtrip needs PCRs to read",
ctx.persona.id
),
});
}
if !binary_on_path("swtpm") {
return Some(ScenarioResult::Skip {
reason: "swtpm not on PATH (Debian: apt install swtpm); \
manifest-roundtrip 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 tpm_persona_yaml() -> &'static str {
r"
schema_version: 1
id: test-tpm
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: '2.0'
"
}
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 = AttestationRoundtrip;
assert_eq!(s.name(), "attestation-roundtrip");
assert!(s.description().contains("manifest-roundtrip"));
assert!(s.description().contains("attestation-manifest.md"));
}
#[test]
fn skips_when_stick_missing() {
let s = AttestationRoundtrip;
let result = s
.run(&fake_ctx(
make_persona(tpm_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_persona_has_no_tpm() {
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(tpm_persona_yaml());
p.tpm.version = TpmVersion::None;
let s = AttestationRoundtrip;
let result = s.run(&fake_ctx(p, stick)).unwrap();
match result {
ScenarioResult::Skip { reason } => {
assert!(
reason.contains("no TPM"),
"expected 'no TPM' in skip 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(tpm_persona_yaml());
p.secure_boot.ovmf_variant = OvmfVariant::Disabled;
let s = AttestationRoundtrip;
let result = s.run(&fake_ctx(p, stick)).unwrap();
match result {
ScenarioResult::Skip { reason } => {
assert!(
reason.contains("ovmf_variant=disabled"),
"got reason: {reason}"
);
}
other => panic!("expected Skip, got {other:?}"),
}
}
#[test]
fn accepts_setup_mode_and_custom_pk() {
let tmp = tempfile::tempdir().unwrap();
let stick = tmp.path().join("fake-stick.img");
std::fs::write(&stick, b"placeholder").unwrap();
for variant in [OvmfVariant::SetupMode, OvmfVariant::CustomPk] {
let mut p = make_persona(tpm_persona_yaml());
p.secure_boot.ovmf_variant = variant;
let result = check_skip_gates(&fake_ctx(p, stick.clone()));
if let Some(ScenarioResult::Skip { reason }) = result {
assert!(
!reason.contains("ovmf_variant=disabled"),
"gate must not skip {variant:?} as if it were disabled: {reason}"
);
}
}
}
#[test]
fn failure_landmarks_are_disjoint_from_pass_landmarks() {
for fail in FAILURE_LANDMARKS {
for pass in TEST_LANDMARKS {
assert!(
!pass.contains(fail) && !fail.contains(pass),
"FAILURE_LANDMARK '{fail}' overlaps with TEST_LANDMARK '{pass}'"
);
}
}
}
}