diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 1bda1b0f5236..f892390cefd5 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -730,7 +730,13 @@ in greetd-no-shadow = runTest ./greetd-no-shadow.nix; grocy = runTest ./grocy.nix; grow-partition = runTest ./grow-partition.nix; - grub = runTest ./grub.nix; + grub = { + basic = runTest ./grub/basic.nix; + efi = runTest ./grub/efi.nix; + graphical = runTest ./grub/graphical.nix; + hashed-password = runTest ./grub/hashed-password.nix; + mirrored-boots = runTest ./grub/mirrored-boots.nix; + }; gs1200-exporter = runTest ./gs1200-exporter.nix; guacamole-server = runTest ./guacamole-server.nix; guix = handleTest ./guix { }; diff --git a/nixos/tests/grub.nix b/nixos/tests/grub/basic.nix similarity index 100% rename from nixos/tests/grub.nix rename to nixos/tests/grub/basic.nix diff --git a/nixos/tests/grub/efi.nix b/nixos/tests/grub/efi.nix new file mode 100644 index 000000000000..953548bb24bb --- /dev/null +++ b/nixos/tests/grub/efi.nix @@ -0,0 +1,45 @@ +{ lib, ... }: +{ + name = "grub-efi"; + + meta = with lib.maintainers; { + maintainers = [ + tomfitzhenry + rnhmjoj + ]; + }; + + nodes.machine = + { ... }: + { + virtualisation.useBootLoader = true; + virtualisation.useEFIBoot = true; + + boot.loader.grub = { + enable = true; + efiSupport = true; + device = "nodev"; + + # Read GRUB from the serial console so its output can be matched + # deterministically with wait_for_console_text, rather than via OCR. + extraConfig = "serial; terminal_output serial"; + }; + boot.loader.efi.canTouchEfiVariables = true; + }; + + testScript = '' + machine.start() + + with subtest("Enters GRUB"): + machine.wait_for_console_text("GNU GRUB") + + with subtest("Loads kernel"): + machine.wait_for_console_text("Linux version") + + with subtest("Reaches multi-user target"): + machine.wait_for_unit("multi-user.target") + + with subtest("Boots via UEFI"): + machine.succeed("test -d /sys/firmware/efi") + ''; +} diff --git a/nixos/tests/grub/graphical.nix b/nixos/tests/grub/graphical.nix new file mode 100644 index 000000000000..54d25ce3d843 --- /dev/null +++ b/nixos/tests/grub/graphical.nix @@ -0,0 +1,57 @@ +{ lib, ... }: +{ + name = "grub-graphical"; + + meta = with lib.maintainers; { + maintainers = [ + tomfitzhenry + rnhmjoj + ]; + }; + + nodes.machine = + { pkgs, ... }: + let + # GRUB only draws a background image when it is in graphical gfxterm + # mode. Bake a marker string into the splash so the OCR check below can + # only succeed when gfxterm actually rendered it; a silent fallback to + # text mode would show the plain menu without this image (and its text). + # + # The image must be an 8-bit sRGB PNG, otherwise GRUB's png module fails + # to load it and silently falls back to its default (non-graphical) menu. + splash = pkgs.runCommand "grub-gfxterm-splash.png" { nativeBuildInputs = [ pkgs.imagemagick ]; } '' + magick -size 1024x768 xc:'#2d2d2d' \ + -font ${pkgs.dejavu_fonts.minimal}/share/fonts/truetype/DejaVuSans.ttf \ + -gravity south -pointsize 48 -fill white \ + -annotate +0+150 'GFXTERMOK' \ + -depth 8 -type TrueColor PNG24:$out + ''; + in + { + virtualisation.useBootLoader = true; + + # Leave the menu up long enough for OCR to catch it; we boot early with a + # keypress once it has, so this does not slow the test down. + boot.loader.timeout = 30; + boot.loader.grub = { + enable = true; + splashImage = splash; + }; + }; + + enableOCR = true; + + testScript = '' + machine.start() + + with subtest("GRUB renders its menu graphically (gfxterm), showing the splash"): + # The marker text lives inside the background image, so reading it back + # proves GRUB displayed the image rather than falling back to text mode. + machine.wait_for_text("GFXTERMOK") + machine.screenshot("grub_gfxterm") + + with subtest("Machine boots into NixOS from GRUB"): + machine.send_key("ret") + machine.wait_for_unit("multi-user.target") + ''; +} diff --git a/nixos/tests/grub/hashed-password.nix b/nixos/tests/grub/hashed-password.nix new file mode 100644 index 000000000000..191468f8b69a --- /dev/null +++ b/nixos/tests/grub/hashed-password.nix @@ -0,0 +1,77 @@ +{ lib, ... }: +{ + name = "grub-hashed-password"; + + meta = with lib.maintainers; { + maintainers = [ + tomfitzhenry + rnhmjoj + ]; + }; + + nodes.machine = + { pkgs, ... }: + let + mkGrubPbkdf2HashFile = + password: + toString ( + pkgs.runCommandLocal "grub-pbkdf2-hash" { nativeBuildInputs = [ pkgs.grub2 ]; } '' + printf "%s\n%s\n" "${password}" "${password}" | grub-mkpasswd-pbkdf2 | grep -o 'grub\.pbkdf2\.[^[:space:]]*' > $out + '' + ); + in + { + virtualisation.useBootLoader = true; + + boot.loader.timeout = null; + boot.loader.grub = { + enable = true; + users.bob.hashedPasswordFile = mkGrubPbkdf2HashFile "bobsecret"; + + # Read GRUB from the serial console so its output can be matched + # deterministically; OCR would work but is flakier, which matters for + # the multi-step interactive login exercised below. + extraConfig = "serial; terminal_output serial"; + }; + }; + + testScript = '' + def grub_login_as(user, password): + """ + Enters user and password to log into GRUB + """ + machine.wait_for_console_text("Enter username:") + machine.send_chars(user + "\n") + machine.wait_for_console_text("Enter password:") + machine.send_chars(password + "\n") + + + def grub_select_all_configurations(): + """ + Selects "All configurations" from the GRUB menu + to trigger a login request. + """ + machine.send_monitor_command("sendkey down") + machine.send_monitor_command("sendkey ret") + + + machine.start() + + # wait for grub screen + machine.wait_for_console_text("GNU GRUB") + + grub_select_all_configurations() + with subtest("Invalid hashedPassword credentials are rejected"): + grub_login_as("bob", "wrongsecret") + machine.wait_for_console_text("access denied") + + grub_select_all_configurations() + with subtest("Valid hashedPassword credentials are accepted"): + grub_login_as("bob", "bobsecret") + machine.send_chars("\n") # press enter to boot + machine.wait_for_console_text("Linux version") + + with subtest("Machine boots correctly"): + machine.wait_for_unit("multi-user.target") + ''; +} diff --git a/nixos/tests/grub/mirrored-boots.nix b/nixos/tests/grub/mirrored-boots.nix new file mode 100644 index 000000000000..f3059bf675bc --- /dev/null +++ b/nixos/tests/grub/mirrored-boots.nix @@ -0,0 +1,61 @@ +{ lib, ... }: +{ + name = "grub-mirrored-boots"; + + meta = with lib.maintainers; { + maintainers = [ + tomfitzhenry + rnhmjoj + ]; + }; + + nodes.machine = + { lib, ... }: + { + virtualisation.useBootLoader = true; + + boot.loader.timeout = null; + boot.loader.grub = { + enable = true; + device = lib.mkOverride 0 ""; + mirroredBoots = [ + { + path = "/boot1"; + devices = [ "/dev/vda" ]; + } + { + path = "/boot2"; + devices = [ "nodev" ]; + } + ]; + + # Read GRUB from the serial console so its output can be matched + # deterministically with wait_for_console_text, rather than via OCR. + extraConfig = "serial; terminal_output serial"; + }; + }; + + testScript = '' + machine.start() + + # wait for grub screen + machine.wait_for_console_text("GNU GRUB") + + machine.send_chars("\n") # press enter to boot default option + + with subtest("Machine boots correctly"): + machine.wait_for_unit("multi-user.target") + + with subtest("Verify boot path 1 GRUB installation and configuration"): + machine.succeed("test -d /boot1/grub") + machine.succeed("test -f /boot1/grub/grub.cfg") + machine.succeed("test -f /boot1/grub/state") + machine.succeed("grep -q 'menuentry' /boot1/grub/grub.cfg") + + with subtest("Verify boot path 2 GRUB installation and configuration"): + machine.succeed("test -d /boot2/grub") + machine.succeed("test -f /boot2/grub/grub.cfg") + machine.succeed("test -f /boot2/grub/state") + machine.succeed("grep -q 'menuentry' /boot2/grub/grub.cfg") + ''; +} diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix index 930d3afc203d..d90fbafd93e8 100644 --- a/nixos/tests/installer.nix +++ b/nixos/tests/installer.nix @@ -1467,7 +1467,6 @@ in # Full disk encryption (root, kernel and initrd encrypted) using GRUB, GPT/UEFI, # LVM-on-LUKS and a keyfile in initrd.secrets to enter the passphrase once fullDiskEncryption = makeInstallerTest "fullDiskEncryption" { - broken = true; createPartitions = '' installer.succeed( "flock /dev/vda parted --script /dev/vda -- mklabel gpt" @@ -1508,6 +1507,53 @@ in ''; enableOCR = true; postBootCommands = '' + target.wait_for_text("Enter passphrase for") + # GRUB's EFI keyboard input appears to drop characters when typed at the + # default speed (producing an "Invalid passphrase" error), so type slowly. + target.send_chars("supersecret\n", 0.2) + ''; + }; + + # Root, kernel and initrd encrypted using GRUB cryptodisk, MBR/legacy BIOS, + # plain LUKS and a keyfile in initrd.secrets to enter the passphrase once + grubCryptodiskLegacyBios = makeInstallerTest "grubCryptodiskLegacyBios" { + meta.maintainers = [ maintainers.tomfitzhenry ]; + createPartitions = '' + installer.succeed( + "flock /dev/vda parted --script /dev/vda -- mklabel msdos mkpart primary ext4 1MiB -1GiB mkpart primary linux-swap -1GiB 100%", + "udevadm settle", + "echo -n supersecret | cryptsetup luksFormat -q --pbkdf-force-iterations 1000 --type luks1 /dev/vda1 -", + "echo -n supersecret | cryptsetup luksOpen /dev/vda1 cryptroot", + "mkfs.ext4 -L nixos /dev/mapper/cryptroot", + "mkswap -L swap /dev/vda2", + "swapon -L swap", + "mount LABEL=nixos /mnt", + "mkdir -p /mnt/etc/nixos", + # Add a keyfile so stage 1 can unlock the root device without a second, + # interactive prompt. This keeps the test independent of the stage 1 + # implementation, which matters because the scripted and systemd + # initrds word their passphrase prompts differently. Only GRUB (which + # is stage 1 independent) then prompts interactively. + "dd if=/dev/urandom of=/mnt/etc/nixos/luks.key bs=256 count=1", + "echo -n supersecret | cryptsetup luksAddKey -q --pbkdf-force-iterations 1000 --key-file - /dev/vda1 /mnt/etc/nixos/luks.key", + ) + ''; + bootLoader = "grub"; + # GRUB draws its cryptodisk passphrase prompt on the console terminal + # without a trailing newline, so the line-based wait_for_console_text can + # never observe it. Use OCR (wait_for_text) to read it off the screen. + enableOCR = true; + extraConfig = '' + boot.loader.grub.enableCryptodisk = true; + boot.initrd.secrets."/luks.key" = "/etc/nixos/luks.key"; + boot.initrd.luks.devices.cryptroot = { + device = lib.mkForce "/dev/vda1"; + keyFile = "/luks.key"; + }; + ''; + postBootCommands = '' + # GRUB has to unlock the disk to read /boot before it can boot the kernel; + # stage 1 then unlocks the root device with the embedded keyfile. target.wait_for_text("Enter passphrase for") target.send_chars("supersecret\n") '';