diff --git a/rust/tool/tests/common/mod.rs b/rust/tool/tests/common/mod.rs index aac101d..0c65602 100644 --- a/rust/tool/tests/common/mod.rs +++ b/rust/tool/tests/common/mod.rs @@ -19,15 +19,25 @@ use sha2::{Digest, Sha256}; /// Create a mock generation link. /// -/// Creates the generation link using the specified version inside a mock profiles directory -/// (mimicking /nix/var/nix/profiles). Returns the path to the generation link. +/// Works like `setup_generation_link_from_toplevel` but already sets up toplevel. pub fn setup_generation_link( tmpdir: &Path, profiles_directory: &Path, version: u64, ) -> Result { let toplevel = setup_toplevel(tmpdir).context("Failed to setup toplevel")?; + setup_generation_link_from_toplevel(&toplevel, profiles_directory, version) +} +/// Create a mock generation link. +/// +/// Creates the generation link using the specified version inside a mock profiles directory +/// (mimicking /nix/var/nix/profiles). Returns the path to the generation link. +pub fn setup_generation_link_from_toplevel( + toplevel: &Path, + profiles_directory: &Path, + version: u64, +) -> Result { let bootspec = json!({ "v1": { "init": format!("init-v{}", version), @@ -70,7 +80,7 @@ pub fn setup_generation_link( /// /// Accepts the temporary directory as a parameter so that the invoking function retains control of /// it (and when it goes out of scope). -fn setup_toplevel(tmpdir: &Path) -> Result { +pub fn setup_toplevel(tmpdir: &Path) -> Result { // Generate a random toplevel name so that multiple toplevel paths can live alongside each // other in the same directory. let toplevel = tmpdir.join(format!("toplevel-{}", random_string(8))); diff --git a/rust/tool/tests/install.rs b/rust/tool/tests/install.rs new file mode 100644 index 0000000..ccb4c08 --- /dev/null +++ b/rust/tool/tests/install.rs @@ -0,0 +1,121 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +use tempfile::{tempdir, TempDir}; + +mod common; + +use common::{ + count_files, hash_file, remove_signature, setup_generation_link_from_toplevel, verify_signature, +}; + +/// Install two generations that point at the same toplevel. +/// This should install two lanzaboote images and one kernel and one initrd. +#[test] +fn do_not_install_duplicates() -> Result<()> { + let esp = tempdir()?; + let tmpdir = tempdir()?; + let profiles = tempdir()?; + let toplevel = common::setup_toplevel(tmpdir.path())?; + + let generation_link1 = setup_generation_link_from_toplevel(&toplevel, profiles.path(), 1)?; + let generation_link2 = setup_generation_link_from_toplevel(&toplevel, profiles.path(), 2)?; + let generation_links = vec![generation_link1, generation_link2]; + + let stub_count = || count_files(&esp.path().join("EFI/Linux")).unwrap(); + let kernel_and_initrd_count = || count_files(&esp.path().join("EFI/nixos")).unwrap(); + + let output1 = common::lanzaboote_install(0, esp.path(), generation_links)?; + assert!(output1.status.success()); + assert_eq!(stub_count(), 2, "Wrong number of stubs after installation"); + assert_eq!( + kernel_and_initrd_count(), + 2, + "Wrong number of kernels & initrds after installation" + ); + Ok(()) +} + +#[test] +fn overwrite_unsigned_images() -> Result<()> { + let esp = tempdir()?; + let tmpdir = tempdir()?; + let profiles = tempdir()?; + + let image1 = image_path(&esp, 1); + let image2 = image_path(&esp, 2); + + let generation_link1 = common::setup_generation_link(tmpdir.path(), profiles.path(), 1)?; + let generation_link2 = common::setup_generation_link(tmpdir.path(), profiles.path(), 2)?; + let generation_links = vec![generation_link1, generation_link2]; + + let output1 = common::lanzaboote_install(0, esp.path(), generation_links.clone())?; + assert!(output1.status.success()); + + remove_signature(&image1)?; + assert!(!verify_signature(&image1)?); + assert!(verify_signature(&image2)?); + + let output2 = common::lanzaboote_install(0, esp.path(), generation_links)?; + assert!(output2.status.success()); + + assert!(verify_signature(&image1)?); + assert!(verify_signature(&image2)?); + + Ok(()) +} + +#[test] +fn overwrite_unsigned_files() -> Result<()> { + let esp = tempdir()?; + let tmpdir = tempdir()?; + let profiles = tempdir()?; + let toplevel = common::setup_toplevel(tmpdir.path())?; + + let generation_link = setup_generation_link_from_toplevel(&toplevel, profiles.path(), 1)?; + let generation_links = vec![generation_link]; + + let kernel_hash_source = hash_file(&toplevel.join("kernel")); + + let nixos_dir = esp.path().join("EFI/nixos"); + let kernel_path = nixos_dir.join(nixos_path(toplevel.join("kernel"), "bzImage")?); + + fs::create_dir_all(&nixos_dir)?; + fs::write(&kernel_path, b"Existing kernel")?; + let kernel_hash_existing = hash_file(&kernel_path); + + let output0 = common::lanzaboote_install(1, esp.path(), generation_links)?; + assert!(output0.status.success()); + + let kernel_hash_overwritten = hash_file(&kernel_path); + + // Assert existing kernel was overwritten. + assert_ne!(kernel_hash_existing, kernel_hash_overwritten); + // Assert overwritten kernel is the source kernel. + assert_eq!(kernel_hash_source, kernel_hash_overwritten); + + Ok(()) +} + +fn image_path(esp: &TempDir, version: u64) -> PathBuf { + esp.path() + .join(format!("EFI/Linux/nixos-generation-{version}.efi")) +} + +fn nixos_path(path: impl AsRef, name: &str) -> Result { + let resolved = path + .as_ref() + .read_link() + .unwrap_or_else(|_| path.as_ref().into()); + + let parent_final_component = resolved + .parent() + .and_then(|x| x.file_name()) + .and_then(|x| x.to_str()) + .with_context(|| format!("Failed to extract final component from: {:?}", resolved))?; + + let nixos_filename = format!("{}-{}.efi", parent_final_component, name); + + Ok(PathBuf::from(nixos_filename)) +}