diff --git a/flake.nix b/flake.nix index 9d74262..5c9d2a0 100644 --- a/flake.nix +++ b/flake.nix @@ -30,38 +30,44 @@ ]; }; - lib = pkgs.lib; + testPkgs = import nixpkgs-test { system = "x86_64-linux"; }; + + inherit (pkgs) lib; rust-nightly = pkgs.rust-bin.fromRustupToolchainFile ./rust/lanzaboote/rust-toolchain.toml; craneLib = crane.lib.x86_64-linux.overrideToolchain rust-nightly; - uefi-run = pkgs.callPackage ./nix/uefi-run.nix { + uefi-run = pkgs.callPackage ./nix/packages/uefi-run.nix { inherit craneLib; }; # Build attributes for a Rust application. - buildRustApp = { - src, target ? null, doCheck ? true - }: let - cleanedSrc = craneLib.cleanCargoSource src; - commonArgs = { - src = cleanedSrc; - CARGO_BUILD_TARGET = target; - inherit doCheck; + buildRustApp = + { src + , target ? null + , doCheck ? true + }: + let + cleanedSrc = craneLib.cleanCargoSource src; + commonArgs = { + src = cleanedSrc; + CARGO_BUILD_TARGET = target; + inherit doCheck; + }; + + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + in + { + package = craneLib.buildPackage (commonArgs // { + inherit cargoArtifacts; + }); + + clippy = craneLib.cargoClippy (commonArgs // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "-- --deny warnings"; + }); }; - cargoArtifacts = craneLib.buildDepsOnly commonArgs; - in { - package = craneLib.buildPackage (commonArgs // { - inherit cargoArtifacts; - }); - - clippy = craneLib.cargoClippy (commonArgs // { - inherit cargoArtifacts; - cargoClippyExtraArgs = "-- --deny warnings"; - }); - }; - lanzabooteCrane = buildRustApp { src = ./rust/lanzaboote; target = "x86_64-unknown-uefi"; @@ -76,9 +82,10 @@ lanzatool-unwrapped = lanzatoolCrane.package; - lanzatool = pkgs.runCommand "lanzatool" { - nativeBuildInputs = [ pkgs.makeWrapper ]; - } '' + lanzatool = pkgs.runCommand "lanzatool" + { + nativeBuildInputs = [ pkgs.makeWrapper ]; + } '' mkdir -p $out/bin # Clean PATH to only contain what we need to do objcopy. Also @@ -88,13 +95,14 @@ --set RUST_BACKTRACE full \ --set LANZABOOTE_STUB ${lanzaboote}/bin/lanzaboote.efi ''; - in { + in + { overlays.default = final: prev: { inherit lanzatool; }; nixosModules.lanzaboote = { pkgs, lib, ... }: { - imports = [ ./nix/lanzaboote.nix ]; + imports = [ ./nix/modules/lanzaboote.nix ]; boot.lanzaboote.package = lib.mkDefault self.packages.${pkgs.system}.lanzatool; }; @@ -115,6 +123,8 @@ pkgs.efitools pkgs.python39Packages.ovmfvartool pkgs.qemu + pkgs.nixpkgs-fmt + pkgs.statix ]; inputsFrom = [ @@ -122,159 +132,12 @@ ]; }; - checks.x86_64-linux = let - mkSecureBootTest = { name, machine ? {}, testScript }: nixpkgs-test.legacyPackages.x86_64-linux.nixosTest { - inherit name testScript; - nodes.machine = { lib, ... }: { - imports = [ - self.nixosModules.lanzaboote - machine - ]; - - nixpkgs.overlays = [ self.overlays.default ]; - - virtualisation = { - useBootLoader = true; - useEFIBoot = true; - useSecureBoot = true; - }; - - boot.loader.efi = { - canTouchEfiVariables = true; - }; - boot.lanzaboote = { - enable = true; - enrollKeys = lib.mkDefault true; - pkiBundle = ./pki; - }; - }; - }; - - # Execute a boot test that is intended to fail. - # - mkUnsignedTest = { name, path, appendCrap ? false }: mkSecureBootTest { - inherit name; - testScript = '' - import json - import os.path - bootspec = None - - def convert_to_esp(store_file_path): - store_dir = os.path.basename(os.path.dirname(store_file_path)) - filename = os.path.basename(store_file_path) - return f'/boot/EFI/nixos/{store_dir}-{filename}.efi' - - machine.start() - bootspec = json.loads(machine.succeed("cat /run/current-system/boot.json")).get('v1') - assert bootspec is not None, "Unsupported bootspec version!" - src_path = ${path.src} - dst_path = ${path.dst} - machine.succeed(f"cp -rf {src_path} {dst_path}") - '' + lib.optionalString appendCrap '' - machine.succeed(f"echo Foo >> {dst_path}") - '' + - '' - machine.succeed("sync") - machine.crash() - machine.start() - machine.wait_for_console_text("Hash mismatch") - ''; - }; - in - { - lanzatool-clippy = lanzatoolCrane.clippy; - lanzaboote-clippy = lanzabooteCrane.clippy; - - # TODO: user mode: OK - # TODO: how to get in: {deployed, audited} mode ? - lanzaboote-boot = mkSecureBootTest { - name = "signed-files-boot-under-secureboot"; - testScript = '' - machine.start() - assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") - ''; - }; - - lanzaboote-boot-under-sd-stage1 = mkSecureBootTest { - name = "signed-files-boot-under-secureboot-systemd-stage-1"; - machine = { ... }: { - boot.initrd.systemd.enable = true; - }; - testScript = '' - machine.start() - assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") - ''; - }; - - # So, this is the responsibility of the lanzatool install - # to run the append-initrd-secret script - # This test assert that lanzatool still do the right thing - # preDeviceCommands should not have any root filesystem mounted - # so it should not be able to find /etc/iamasecret, other than the - # initrd's one. - # which should exist IF lanzatool do the right thing. - lanzaboote-with-initrd-secrets = mkSecureBootTest { - name = "signed-files-boot-with-secrets-under-secureboot"; - machine = { ... }: { - boot.initrd.secrets = { - "/etc/iamasecret" = (pkgs.writeText "iamsecret" "this is a very secure secret"); - }; - - boot.initrd.preDeviceCommands = '' - grep "this is a very secure secret" /etc/iamasecret - ''; - }; - testScript = '' - machine.start() - assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") - ''; - }; - - # The initrd is not directly signed. Its hash is embedded - # into lanzaboote. To make integrity verification fail, we - # actually have to modify the initrd. Appending crap to the - # end is a harmless way that would make the kernel still - # accept it. - is-initrd-secured = mkUnsignedTest { - name = "unsigned-initrd-do-not-boot-under-secureboot"; - path = { - src = "bootspec.get('initrd')"; - dst = "convert_to_esp(bootspec.get('initrd'))"; - }; - appendCrap = true; - }; - - is-kernel-secured = mkUnsignedTest { - name = "unsigned-kernel-do-not-boot-under-secureboot"; - path = { - src = "bootspec.get('kernel')"; - dst = "convert_to_esp(bootspec.get('kernel'))"; - }; - }; - specialisation-works = mkSecureBootTest { - name = "specialisation-still-boot-under-secureboot"; - machine = { pkgs, ... }: { - specialisation.variant.configuration = { - environment.systemPackages = [ - pkgs.efibootmgr - ]; - }; - }; - testScript = '' - machine.start() - print(machine.succeed("ls -lah /boot/EFI/Linux")) - print(machine.succeed("cat /run/current-system/boot.json")) - # TODO: make it more reliable to find this filename, i.e. read it from somewhere? - machine.succeed("bootctl set-default nixos-generation-1-specialisation-variant.efi") - machine.succeed("sync") - machine.fail("efibootmgr") - machine.crash() - machine.start() - print(machine.succeed("bootctl")) - # We have efibootmgr in this specialisation. - machine.succeed("efibootmgr") - ''; - }; - }; + checks.x86_64-linux = { + lanzatool-clippy = lanzatoolCrane.clippy; + lanzaboote-clippy = lanzabooteCrane.clippy; + } // (import ./nix/tests/lanzaboote.nix { + inherit pkgs testPkgs; + lanzabooteModule = self.nixosModules.lanzaboote; + }); }; } diff --git a/nix/lanzaboote.nix b/nix/modules/lanzaboote.nix similarity index 98% rename from nix/lanzaboote.nix rename to nix/modules/lanzaboote.nix index d223b19..3942e5b 100644 --- a/nix/lanzaboote.nix +++ b/nix/modules/lanzaboote.nix @@ -1,4 +1,4 @@ -{ lib, config, pkgs, ... }: +{ lib, config, pkgs, ... }: with lib; let cfg = config.boot.lanzaboote; diff --git a/nix/uefi-run.nix b/nix/packages/uefi-run.nix similarity index 100% rename from nix/uefi-run.nix rename to nix/packages/uefi-run.nix diff --git a/nix/tests/lanzaboote.nix b/nix/tests/lanzaboote.nix new file mode 100644 index 0000000..2ec5b96 --- /dev/null +++ b/nix/tests/lanzaboote.nix @@ -0,0 +1,156 @@ +{ pkgs +, testPkgs +, lanzabooteModule +}: + +let + inherit (pkgs) lib; + + mkSecureBootTest = { name, machine ? { }, testScript }: testPkgs.nixosTest { + inherit name testScript; + nodes.machine = { lib, ... }: { + imports = [ + lanzabooteModule + machine + ]; + + virtualisation = { + useBootLoader = true; + useEFIBoot = true; + useSecureBoot = true; + }; + + boot.loader.efi = { + canTouchEfiVariables = true; + }; + boot.lanzaboote = { + enable = true; + enrollKeys = lib.mkDefault true; + pkiBundle = ../../pki; + }; + }; + }; + + # Execute a boot test that is intended to fail. + # + mkUnsignedTest = { name, path, appendCrap ? false }: mkSecureBootTest { + inherit name; + testScript = '' + import json + import os.path + bootspec = None + + def convert_to_esp(store_file_path): + store_dir = os.path.basename(os.path.dirname(store_file_path)) + filename = os.path.basename(store_file_path) + return f'/boot/EFI/nixos/{store_dir}-{filename}.efi' + + machine.start() + bootspec = json.loads(machine.succeed("cat /run/current-system/boot.json")).get('v1') + assert bootspec is not None, "Unsupported bootspec version!" + src_path = ${path.src} + dst_path = ${path.dst} + machine.succeed(f"cp -rf {src_path} {dst_path}") + '' + lib.optionalString appendCrap '' + machine.succeed(f"echo Foo >> {dst_path}") + '' + + '' + machine.succeed("sync") + machine.crash() + machine.start() + machine.wait_for_console_text("Hash mismatch") + ''; + }; +in +{ + # TODO: user mode: OK + # TODO: how to get in: {deployed, audited} mode ? + lanzaboote-boot = mkSecureBootTest { + name = "signed-files-boot-under-secureboot"; + testScript = '' + machine.start() + assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") + ''; + }; + + lanzaboote-boot-under-sd-stage1 = mkSecureBootTest { + name = "signed-files-boot-under-secureboot-systemd-stage-1"; + machine = { ... }: { + boot.initrd.systemd.enable = true; + }; + testScript = '' + machine.start() + assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") + ''; + }; + + # So, this is the responsibility of the lanzatool install + # to run the append-initrd-secret script + # This test assert that lanzatool still do the right thing + # preDeviceCommands should not have any root filesystem mounted + # so it should not be able to find /etc/iamasecret, other than the + # initrd's one. + # which should exist IF lanzatool do the right thing. + lanzaboote-with-initrd-secrets = mkSecureBootTest { + name = "signed-files-boot-with-secrets-under-secureboot"; + machine = { ... }: { + boot.initrd.secrets = { + "/etc/iamasecret" = (pkgs.writeText "iamsecret" "this is a very secure secret"); + }; + + boot.initrd.preDeviceCommands = '' + grep "this is a very secure secret" /etc/iamasecret + ''; + }; + testScript = '' + machine.start() + assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") + ''; + }; + + # The initrd is not directly signed. Its hash is embedded + # into lanzaboote. To make integrity verification fail, we + # actually have to modify the initrd. Appending crap to the + # end is a harmless way that would make the kernel still + # accept it. + is-initrd-secured = mkUnsignedTest { + name = "unsigned-initrd-do-not-boot-under-secureboot"; + path = { + src = "bootspec.get('initrd')"; + dst = "convert_to_esp(bootspec.get('initrd'))"; + }; + appendCrap = true; + }; + + is-kernel-secured = mkUnsignedTest { + name = "unsigned-kernel-do-not-boot-under-secureboot"; + path = { + src = "bootspec.get('kernel')"; + dst = "convert_to_esp(bootspec.get('kernel'))"; + }; + }; + specialisation-works = mkSecureBootTest { + name = "specialisation-still-boot-under-secureboot"; + machine = { pkgs, ... }: { + specialisation.variant.configuration = { + environment.systemPackages = [ + pkgs.efibootmgr + ]; + }; + }; + testScript = '' + machine.start() + print(machine.succeed("ls -lah /boot/EFI/Linux")) + print(machine.succeed("cat /run/current-system/boot.json")) + # TODO: make it more reliable to find this filename, i.e. read it from somewhere? + machine.succeed("bootctl set-default nixos-generation-1-specialisation-variant.efi") + machine.succeed("sync") + machine.fail("efibootmgr") + machine.crash() + machine.start() + print(machine.succeed("bootctl")) + # We have efibootmgr in this specialisation. + machine.succeed("efibootmgr") + ''; + }; +}