//! GRUB menu rendering for the Secure Boot chain (v0.7.0). //! //! Secure-Boot-enabled firmware refuses our unsigned iPXE, so those //! clients are automatically escalated (see `openpxe_dhcp_proxy:: //! escalation`) to the Microsoft-signed Fedora `shim` → signed `grub` //! chain. GRUB then fetches `grub.cfg` from this server (TFTP `$prefix` //! resolution, or HTTP when the whole chain came over HTTP Boot) — and //! this module renders that config from the same boot-entry model that //! renders `boot.ipxe`. //! //! Scope: **Linux kernel entries only.** A signed GRUB will only execute //! kernels that pass shim verification — i.e. distro-signed kernels — //! which is exactly what `LinuxKernel` boot entries point at. `sanboot` //! ISO emulation and `wimboot` are iPXE mechanisms with no signed //! equivalent; those entries are omitted here, and the menu says so. //! (Windows deployment under Secure Boot has no legitimate unsigned //! path — per project policy we never ship test-signed binaries or touch //! client trust stores.) //! //! The kernel/initrd lines use GRUB's `(http,host:port)` device syntax; //! Fedora's signed netboot GRUB carries the `http`, `tftp` and `efinet` //! modules built in, so no unsigned module loading is required. use openpxe_iso_store::{BootKind, IsoMeta}; use std::fmt::Write as _; /// Render the full `grub.cfg` for the signed-GRUB menu. /// /// `base_url` is the public HTTP base (`http://10.0.0.5` or /// `http://10.0.0.5:8080`) — converted to GRUB's `(http,host:port)` /// device prefix for kernel/initrd fetches. #[must_use] pub fn render_grub_menu(isos: &[IsoMeta], base_url: &str) -> String { let base = base_url.trim_end_matches('/'); let dev = grub_http_device(base); let mut s = String::new(); let _ = writeln!(s, "# OpenPXE — Secure Boot menu (signed shim+GRUB chain)"); // v0.7.1: before showing the limited signed menu, try to hand the // boot back to full iPXE *in this same boot cycle*. With Secure Boot // OFF the chainload succeeds and the client gets the complete iPXE // feature set (sanboot, wimboot, the full menu) despite having been // escalated here. With Secure Boot ON, shim's verifier refuses the // unsigned image INLINE — no reboot, no failed cycle — and execution // falls through to the signed menu below. The all-drivers build is // used because a MAC only lands here after the firmware-net build // already failed once. let _ = writeln!(s, "if [ \"$grub_cpu\" = \"arm64\" ]; then"); let _ = writeln!(s, " set openpxe_ipxe=ipxe-arm64.efi"); let _ = writeln!(s, "else"); let _ = writeln!(s, " set openpxe_ipxe=ipxe.efi"); let _ = writeln!(s, "fi"); let _ = writeln!(s, "if chainloader {dev}/ipxe/$openpxe_ipxe ; then"); let _ = writeln!(s, " boot"); let _ = writeln!(s, "fi"); let _ = writeln!(s); let _ = writeln!(s, "set timeout=30"); let _ = writeln!(s, "set default=0"); let _ = writeln!(s); let mut entries = 0usize; for iso in isos { for entry in &iso.boot_entries { let BootKind::LinuxKernel { kernel_url, initrd_urls, args, } = &entry.kind else { continue; }; // GRUB menu titles: keep quotes out of the label. let title = entry.title.replace('"', "'"); let cmdline = args.cmdline.replace("${base-url}", base); let _ = writeln!(s, "menuentry \"{} — {title}\" {{", iso.filename); let _ = writeln!(s, " linux {dev}/{kernel_url} {cmdline}"); if !initrd_urls.is_empty() { let _ = write!(s, " initrd"); for u in initrd_urls { let _ = write!(s, " {dev}/{u}"); } let _ = writeln!(s); } let _ = writeln!(s, "}}"); let _ = writeln!(s); entries += 1; } } if entries == 0 { let _ = writeln!( s, "menuentry \"No Secure-Boot-bootable images on this server yet\" {{ true }}" ); let _ = writeln!(s); } // Always give the operator a way off this screen. let _ = writeln!(s, "menuentry \"Boot from local disk\" {{"); let _ = writeln!(s, " exit"); let _ = writeln!(s, "}}"); s } /// `http://10.0.0.5:8080` → `(http,10.0.0.5:8080)`. GRUB wants the /// scheme as the device type and host[:port] as the device address. fn grub_http_device(base: &str) -> String { let host = base .trim_start_matches("http://") .trim_start_matches("https://"); format!("(http,{host})") } #[cfg(test)] mod tests { use super::*; use openpxe_iso_store::{BootEntry, IsoSource, KernelArgs}; fn linux_iso() -> IsoMeta { IsoMeta { id: "alp".into(), filename: "alpine.iso".into(), size_bytes: 1, sha256_hex: None, uploaded_at: time::OffsetDateTime::UNIX_EPOCH, source: IsoSource::Local, introspection: openpxe_iso_store::IntrospectionReport::default(), boot_entries: vec![BootEntry { id: "alp-linux".into(), title: "Linux installer".into(), kind: BootKind::LinuxKernel { kernel_url: "iso/alp/boot/vmlinuz".into(), initrd_urls: vec!["iso/alp/boot/initrd".into()], args: KernelArgs { cmdline: "quiet repo=${base-url}/iso/alp.iso".into(), }, }, }], category: openpxe_iso_store::IsoCategory::default(), password_hash: None, } } #[test] fn renders_linux_entries_with_http_device_urls() { let cfg = render_grub_menu(&[linux_iso()], "http://10.0.0.5:8080/"); assert!( cfg.contains("menuentry \"alpine.iso — Linux installer\""), "{cfg}" ); assert!( cfg.contains("linux (http,10.0.0.5:8080)/iso/alp/boot/vmlinuz quiet repo=http://10.0.0.5:8080/iso/alp.iso"), "{cfg}" ); assert!( cfg.contains("initrd (http,10.0.0.5:8080)/iso/alp/boot/initrd"), "{cfg}" ); assert!(cfg.contains("Boot from local disk"), "{cfg}"); } #[test] fn config_tries_ipxe_chainload_before_menu() { // v0.7.1: SB-off machines recover full iPXE in the same boot; // SB-on machines fail the chainload inline and reach the menu. let cfg = render_grub_menu(&[linux_iso()], "http://10.0.0.5:8080"); let chain_pos = cfg .find("if chainloader (http,10.0.0.5:8080)/ipxe/$openpxe_ipxe ; then") .expect("chainload attempt missing"); let menu_pos = cfg.find("menuentry").expect("menu missing"); assert!( chain_pos < menu_pos, "chainload must precede the menu:\n{cfg}" ); // Arch-conditional binary selection via GRUB's $grub_cpu. assert!(cfg.contains("set openpxe_ipxe=ipxe-arm64.efi"), "{cfg}"); assert!(cfg.contains("set openpxe_ipxe=ipxe.efi"), "{cfg}"); } #[test] fn sanboot_and_wimboot_entries_are_omitted() { let mut iso = linux_iso(); iso.boot_entries = vec![BootEntry { id: "win".into(), title: "Windows".into(), kind: BootKind::SanBootIso { iso_url: "iso/win.iso".into(), }, }]; let cfg = render_grub_menu(&[iso], "http://10.0.0.5"); assert!(!cfg.contains("Windows"), "{cfg}"); assert!(cfg.contains("No Secure-Boot-bootable images"), "{cfg}"); } }