lanzatool: generate custom os-release
This commit is contained in:
parent
2f36f1119a
commit
4f44cb70a2
|
@ -47,7 +47,6 @@ in
|
|||
config = mkIf cfg.enable {
|
||||
boot.bootspec = {
|
||||
enable = true;
|
||||
extensions."lanzaboote"."osRelease" = config.environment.etc."os-release".source;
|
||||
};
|
||||
boot.loader.supportsInitrdSecrets = true;
|
||||
boot.loader.external = {
|
||||
|
|
|
@ -183,6 +183,12 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dissimilar"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd5f0c7e4bd266b8ab2550e6238d2e74977c23c15536ac7be45e9c95e2e3fbbb"
|
||||
|
||||
[[package]]
|
||||
name = "doc-comment"
|
||||
version = "0.3.3"
|
||||
|
@ -195,6 +201,16 @@ version = "1.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
||||
|
||||
[[package]]
|
||||
name = "expect-test"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d4661aca38d826eb7c72fe128e4238220616de4c0cc00db7bfc38e2e1364dd3"
|
||||
dependencies = [
|
||||
"dissimilar",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.8.0"
|
||||
|
@ -204,6 +220,18 @@ dependencies = [
|
|||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.6"
|
||||
|
@ -284,12 +312,15 @@ dependencies = [
|
|||
"blake3",
|
||||
"bootspec",
|
||||
"clap",
|
||||
"expect-test",
|
||||
"filetime",
|
||||
"goblin",
|
||||
"nix",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"time",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
@ -591,6 +622,22 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8"
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.15.0"
|
||||
|
@ -665,3 +712,60 @@ name = "winapi-x86_64-pc-windows-gnu"
|
|||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
|
||||
|
|
|
@ -17,7 +17,10 @@ blake3 = "1.3.3"
|
|||
# TODO: wait for a upstream release and pin it.
|
||||
bootspec = { git = "https://github.com/DeterminateSystems/bootspec" }
|
||||
walkdir = "2.3.2"
|
||||
time = "0.3.17"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.7"
|
||||
expect-test = "1.4.0"
|
||||
filetime = "0.2.19"
|
||||
rand = "0.8.5"
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bootspec::generation::Generation as BootspecGeneration;
|
||||
use bootspec::BootJson;
|
||||
use bootspec::SpecialisationName;
|
||||
use serde::de::IntoDeserializer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecureBootExtension {
|
||||
#[serde(rename = "osRelease")]
|
||||
pub os_release: PathBuf,
|
||||
}
|
||||
|
||||
/// (Possibly) extended Bootspec.
|
||||
///
|
||||
/// This struct currently does not have any extensions. We keep it around so that extension becomes
|
||||
/// easy if/when we have to do it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtendedBootJson {
|
||||
pub bootspec: BootJson,
|
||||
pub extensions: SecureBootExtension,
|
||||
}
|
||||
|
||||
/// A system configuration.
|
||||
|
@ -51,34 +47,19 @@ impl Generation {
|
|||
.try_into()
|
||||
.map_err(|err: &'static str| anyhow!(err))?;
|
||||
|
||||
let extensions = Self::extract_extensions(&bootspec)?;
|
||||
|
||||
Ok(Self {
|
||||
version: link.version,
|
||||
specialisation_name: None,
|
||||
spec: ExtendedBootJson {
|
||||
bootspec,
|
||||
extensions,
|
||||
},
|
||||
spec: ExtendedBootJson { bootspec },
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_extensions(bootspec: &BootJson) -> Result<SecureBootExtension> {
|
||||
Ok(Deserialize::deserialize(
|
||||
bootspec.extensions.get("lanzaboote")
|
||||
.context("Failed to extract Lanzaboote-specific extension from Bootspec, missing lanzaboote field in `extensions`")?
|
||||
.clone()
|
||||
.into_deserializer()
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn specialise(&self, name: &SpecialisationName, bootspec: &BootJson) -> Result<Self> {
|
||||
Ok(Self {
|
||||
version: self.version,
|
||||
specialisation_name: Some(name.clone()),
|
||||
spec: ExtendedBootJson {
|
||||
bootspec: bootspec.clone(),
|
||||
extensions: Self::extract_extensions(bootspec)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -86,6 +67,28 @@ impl Generation {
|
|||
pub fn is_specialized(&self) -> Option<SpecialisationName> {
|
||||
self.specialisation_name.clone()
|
||||
}
|
||||
|
||||
/// Describe the generation in a single line.
|
||||
///
|
||||
/// Emulates how NixOS's current systemd-boot-builder.py describes generations so that the user
|
||||
/// interface remains similar.
|
||||
///
|
||||
/// This is currently implemented by poking around the filesystem to find the necessary data.
|
||||
/// Ideally, the needed data should be included in the bootspec.
|
||||
pub fn describe(&self) -> Result<String> {
|
||||
let toplevel = &self.spec.bootspec.toplevel.0;
|
||||
|
||||
let nixos_version = fs::read_to_string(toplevel.join("nixos-version"))
|
||||
.unwrap_or_else(|_| String::from("Unknown"));
|
||||
let kernel_version =
|
||||
read_kernel_version(toplevel).context("Failed to read kernel version.")?;
|
||||
let build_time = read_build_time(toplevel).unwrap_or_else(|_| String::from("Unknown"));
|
||||
|
||||
Ok(format!(
|
||||
"Generation {} NixOS {}, Linux Kernel {}, Built on {}",
|
||||
self.version, nixos_version, kernel_version, build_time
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Generation {
|
||||
|
@ -94,6 +97,32 @@ impl fmt::Display for Generation {
|
|||
}
|
||||
}
|
||||
|
||||
/// Read the kernel version from the name of a directory inside the toplevel directory.
|
||||
///
|
||||
/// The path looks something like this: $toplevel/kernel-modules/lib/modules/6.1.1
|
||||
fn read_kernel_version(toplevel: &Path) -> Result<String> {
|
||||
let path = fs::read_dir(toplevel.join("kernel-modules/lib/modules"))?
|
||||
.into_iter()
|
||||
.next()
|
||||
.transpose()?
|
||||
.map(|x| x.path())
|
||||
.with_context(|| format!("Failed to read directory {:?}.", toplevel))?;
|
||||
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|x| x.to_str())
|
||||
.context("Failed to convert path to filename string.")?;
|
||||
|
||||
Ok(String::from(file_name))
|
||||
}
|
||||
|
||||
fn read_build_time(path: &Path) -> Result<String> {
|
||||
let build_time = time::OffsetDateTime::from_unix_timestamp(fs::metadata(path)?.mtime())?
|
||||
.date()
|
||||
.to_string();
|
||||
Ok(build_time)
|
||||
}
|
||||
|
||||
/// A link pointing to a generation.
|
||||
///
|
||||
/// Can be built from a symlink in /nix/var/nix/profiles/ alone because the name of the
|
||||
|
|
|
@ -10,6 +10,7 @@ use tempfile::tempdir;
|
|||
use crate::esp::EspPaths;
|
||||
use crate::gc::Roots;
|
||||
use crate::generation::{Generation, GenerationLink};
|
||||
use crate::os_release::OsRelease;
|
||||
use crate::pe;
|
||||
use crate::signature::KeyPair;
|
||||
|
||||
|
@ -113,7 +114,6 @@ impl Installer {
|
|||
|
||||
fn install_generation(&mut self, generation: &Generation) -> Result<()> {
|
||||
let bootspec = &generation.spec.bootspec;
|
||||
let secureboot_extensions = &generation.spec.extensions;
|
||||
|
||||
let esp_paths = EspPaths::new(&self.esp, generation)?;
|
||||
self.gc_roots.extend(esp_paths.to_iter());
|
||||
|
@ -129,6 +129,12 @@ impl Installer {
|
|||
// TODO(Raito): prove to niksnur this is actually acceptable.
|
||||
let secure_temp_dir = tempdir()?;
|
||||
|
||||
let os_release = OsRelease::from_generation(generation)
|
||||
.context("Failed to build OsRelease from generation.")?;
|
||||
let os_release_path = secure_temp_dir.path().join("os-release");
|
||||
fs::write(&os_release_path, os_release.to_string().as_bytes())
|
||||
.with_context(|| format!("Failed to write os-release file: {:?}", os_release_path))?;
|
||||
|
||||
println!("Appending secrets to initrd...");
|
||||
|
||||
let initrd_location = secure_temp_dir.path().join("initrd");
|
||||
|
@ -164,7 +170,7 @@ impl Installer {
|
|||
let lanzaboote_image = pe::lanzaboote_image(
|
||||
&secure_temp_dir,
|
||||
&self.lanzaboote_stub,
|
||||
&secureboot_extensions.os_release,
|
||||
&os_release_path,
|
||||
&kernel_cmdline,
|
||||
&esp_paths.kernel,
|
||||
&esp_paths.initrd,
|
||||
|
|
|
@ -3,6 +3,7 @@ mod esp;
|
|||
mod gc;
|
||||
mod generation;
|
||||
mod install;
|
||||
mod os_release;
|
||||
mod pe;
|
||||
mod signature;
|
||||
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::generation::Generation;
|
||||
|
||||
/// An os-release file represented by a BTreeMap.
|
||||
///
|
||||
/// This is implemented using a map, so that it can be easily extended in the future (e.g. by
|
||||
/// reading the original os-release and patching it).
|
||||
///
|
||||
/// The BTreeMap is used over a HashMap, so that the keys are ordered. This is irrelevant for
|
||||
/// systemd-boot (which does not care about order when reading the os-release file) but is useful
|
||||
/// for testing. Ordered keys allow using snapshot tests.
|
||||
pub struct OsRelease(BTreeMap<&'static str, String>);
|
||||
|
||||
impl OsRelease {
|
||||
pub fn from_generation(generation: &Generation) -> Result<Self> {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
// Because of a null pointer dereference, `bootctl` segfaults when no ID field is present
|
||||
// in the .osrel section of the stub.
|
||||
// Fixed in https://github.com/systemd/systemd/pull/25953
|
||||
//
|
||||
// Because the ID field here does not have the same meaning as in a real os-release file,
|
||||
// it is fine to use a dummy value.
|
||||
map.insert("ID", String::from("lanza"));
|
||||
map.insert("PRETTY_NAME", generation.spec.bootspec.label.clone());
|
||||
map.insert(
|
||||
"VERSION_ID",
|
||||
generation
|
||||
.describe()
|
||||
.context("Failed to describe generation.")?,
|
||||
);
|
||||
|
||||
Ok(Self(map))
|
||||
}
|
||||
}
|
||||
|
||||
/// Display OsRelease in the format of an os-release file.
|
||||
impl fmt::Display for OsRelease {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for (key, value) in &self.0 {
|
||||
writeln!(f, "{}={}", key, value)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ pub fn setup_generation_link(
|
|||
version: u64,
|
||||
) -> Result<PathBuf> {
|
||||
let toplevel = setup_toplevel(tmpdir).context("Failed to setup toplevel")?;
|
||||
// Explicitly set modification time so that snapshot test of os-release reliably works.
|
||||
filetime::set_file_mtime(&toplevel, filetime::FileTime::zero())?;
|
||||
|
||||
let bootspec = json!({
|
||||
"v1": {
|
||||
|
@ -73,7 +75,8 @@ fn setup_toplevel(tmpdir: &Path) -> Result<PathBuf> {
|
|||
let initrd_path = toplevel.join("initrd");
|
||||
let kernel_path = toplevel.join("kernel");
|
||||
let systemd_path = toplevel.join("systemd");
|
||||
let os_release_path = toplevel.join("os-release");
|
||||
let nixos_version_path = toplevel.join("nixos-version");
|
||||
let kernel_modules_path = toplevel.join("kernel-modules/lib/modules/6.1.1");
|
||||
|
||||
// To simplify the test setup, we use the systemd stub for all PE binaries used by lanzatool.
|
||||
// Lanzatool doesn't care whether its actually a kernel or initrd but only whether it can
|
||||
|
@ -82,7 +85,8 @@ fn setup_toplevel(tmpdir: &Path) -> Result<PathBuf> {
|
|||
fs::copy(&test_systemd_stub, initrd_path)?;
|
||||
fs::copy(&test_systemd_stub, kernel_path)?;
|
||||
symlink(&test_systemd, systemd_path)?;
|
||||
setup_os_release(&os_release_path).context("Failed to setup os-release")?;
|
||||
fs::write(nixos_version_path, b"23.05")?;
|
||||
fs::create_dir_all(kernel_modules_path)?;
|
||||
|
||||
Ok(toplevel)
|
||||
}
|
||||
|
@ -95,19 +99,6 @@ fn random_string(length: usize) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn setup_os_release(path: &Path) -> Result<()> {
|
||||
let content = r#"
|
||||
ID=lanzaos
|
||||
NAME=LanzaOS
|
||||
PRETTY_NAME="LanzaOS 23.05 (Goat)"
|
||||
VERSION="23.05 (Goat)"
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(path)?;
|
||||
file.write_all(content.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Call the `lanzaboote install` command.
|
||||
pub fn lanzaboote_install(
|
||||
config_limit: u64,
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
use std::fs;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use expect_test::expect;
|
||||
use tempfile::tempdir;
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn generate_expected_os_release() -> Result<()> {
|
||||
let esp_mountpoint = tempdir()?;
|
||||
let tmpdir = tempdir()?;
|
||||
let profiles = tempdir()?;
|
||||
|
||||
let generation_link = common::setup_generation_link(tmpdir.path(), profiles.path(), 1)
|
||||
.expect("Failed to setup generation link");
|
||||
|
||||
let output0 = common::lanzaboote_install(0, esp_mountpoint.path(), vec![generation_link])?;
|
||||
assert!(output0.status.success());
|
||||
|
||||
let stub_data = fs::read(
|
||||
esp_mountpoint
|
||||
.path()
|
||||
.join("EFI/Linux/nixos-generation-1.efi"),
|
||||
)?;
|
||||
let os_release_section = pe_section(&stub_data, ".osrel")
|
||||
.context("Failed to read .osrelease PE section.")?
|
||||
.to_owned();
|
||||
|
||||
let expected = expect![[r#"
|
||||
ID=lanza
|
||||
PRETTY_NAME=LanzaOS
|
||||
VERSION_ID=Generation 1 NixOS 23.05, Linux Kernel 6.1.1, Built on 1970-01-01
|
||||
"#]];
|
||||
|
||||
expected.assert_eq(&String::from_utf8(os_release_section)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pe_section<'a>(file_data: &'a [u8], section_name: &str) -> Option<&'a [u8]> {
|
||||
let pe_binary = goblin::pe::PE::parse(file_data).ok()?;
|
||||
|
||||
pe_binary
|
||||
.sections
|
||||
.iter()
|
||||
.find(|s| s.name().unwrap() == section_name)
|
||||
.and_then(|s| {
|
||||
let section_start: usize = s.pointer_to_raw_data.try_into().ok()?;
|
||||
assert!(s.virtual_size <= s.size_of_raw_data);
|
||||
let section_end: usize = section_start + usize::try_from(s.virtual_size).ok()?;
|
||||
Some(&file_data[section_start..section_end])
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue