diff --git a/rust/lanzatool/Cargo.lock b/rust/lanzatool/Cargo.lock index 8532f0d..7c9205e 100644 --- a/rust/lanzatool/Cargo.lock +++ b/rust/lanzatool/Cargo.lock @@ -95,6 +95,17 @@ dependencies = [ "wasi", ] +[[package]] +name = "goblin" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572564d6cba7d09775202c8e7eebc4d534d5ae36578ab402fb21e182a0ac9505" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "heck" version = "0.4.0" @@ -110,6 +121,12 @@ dependencies = [ "libc", ] +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + [[package]] name = "lanztool" version = "0.1.0" @@ -117,6 +134,9 @@ dependencies = [ "anyhow", "clap", "ed25519-compact", + "goblin", + "serde", + "serde_json", ] [[package]] @@ -125,6 +145,15 @@ version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + [[package]] name = "once_cell" version = "1.16.0" @@ -137,6 +166,12 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -179,6 +214,63 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdbda6ac5cd1321e724fa9cee216f3a61885889b896f073b8f82322789c5250e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "strsim" version = "0.10.0" diff --git a/rust/lanzatool/Cargo.toml b/rust/lanzatool/Cargo.toml index 339de02..0f2e7cb 100644 --- a/rust/lanzatool/Cargo.toml +++ b/rust/lanzatool/Cargo.toml @@ -9,3 +9,6 @@ edition = "2021" anyhow = "1.0.66" clap = { version = "4.0.26", features = ["derive"] } ed25519-compact = "2.0.2" +goblin = "0.6.0" +serde = { version = "1.0.147", features = ["derive"] } +serde_json = "1.0.89" diff --git a/rust/lanzatool/bootspec.json b/rust/lanzatool/bootspec.json new file mode 100644 index 0000000..912982f --- /dev/null +++ b/rust/lanzatool/bootspec.json @@ -0,0 +1,24 @@ +{ + "v1": { + "init": "/nix/store/7zrsjhxi0c93m2l89rj8jdp9khm8fc6s-nixos-system-tuxedo-22.11.20221115.85d6b39/init", + "initrd": "/nix/store/7a4plccwni1sldhyra75f7m44xgsgiqw-initrd-linux-6.0.8/initrd", + "kernel": "/nix/store/nsw0422iwp4linayqx727pi4fdyja0wv-linux-6.0.8/bzImage", + "kernelParams": [ + "amd_iommu=on", + "amd_iommu=pt", + "iommu=pt", + "kvm.ignore_msrs=1", + "kvm.report_ignored_msrs=0", + "udev.log_priority=3", + "systemd.unified_cgroup_hierarchy=1", + "loglevel=4" + ], + "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", + "toplevel": "/nix/store/7zrsjhxi0c93m2l89rj8jdp9khm8fc6s-nixos-system-tuxedo-22.11.20221115.85d6b39", + "extension": { + "esp": "test", + "bootctl": "/nix/store/89366aivz6v8a34yyni2m04ca9hwrl92-systemd-250.4/bin/bootctl", + "osRelease": "/nix/store/734vglb01ssz73wlihad7xa9yzvwlvx6-etc-os-release" + } + } +} \ No newline at end of file diff --git a/rust/lanzatool/src/bootspec.rs b/rust/lanzatool/src/bootspec.rs new file mode 100644 index 0000000..a2a0c15 --- /dev/null +++ b/rust/lanzatool/src/bootspec.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct Bootspec { + pub v1: GenerationV1, +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct GenerationV1 { + /// Label for the system closure + pub label: String, + /// Path to kernel (bzImage) -- $toplevel/kernel + pub kernel: PathBuf, + /// list of kernel parameters + pub kernel_params: Vec, + /// Path to the init script + pub init: PathBuf, + /// Path to initrd -- $toplevel/initrd + pub initrd: PathBuf, + /// Path to "append-initrd-secrets" script -- $toplevel/append-initrd-secrets + pub initrd_secrets: Option, + /// config.system.build.toplevel path + pub toplevel: PathBuf, + pub extension: Extension, +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Extension { + pub esp: String, + pub bootctl: PathBuf, + pub os_release: PathBuf, +} diff --git a/rust/lanzatool/src/cli.rs b/rust/lanzatool/src/cli.rs index 6576b7c..f643861 100644 --- a/rust/lanzatool/src/cli.rs +++ b/rust/lanzatool/src/cli.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use clap::{Parser, Subcommand}; -use crate::crypto; +use crate::{crypto, install}; #[derive(Parser)] pub struct Cli { @@ -20,6 +20,10 @@ pub enum Commands { Sign { file: PathBuf, private_key: PathBuf }, /// Sign Verify { file: PathBuf, public_key: PathBuf }, + Install { + public_key: PathBuf, + bootspec: PathBuf, + }, } impl Cli { @@ -34,6 +38,10 @@ impl Commands { Commands::Generate => generate(), Commands::Sign { file, private_key } => sign(&file, &private_key), Commands::Verify { file, public_key } => verify(&file, &public_key), + Commands::Install { + public_key, + bootspec, + } => install(&public_key, &bootspec), } } } @@ -77,3 +85,8 @@ fn with_extension(path: &Path, extension: &str) -> PathBuf { file_path.push(extension); PathBuf::from(file_path) } + +fn install(public_key: &Path, bootspec: &Path) -> Result<()> { + let lanzaboote_bin = std::env::var("LANZABOOTE")?; + install::install(public_key, bootspec, Path::new(&lanzaboote_bin)) +} diff --git a/rust/lanzatool/src/esp.rs b/rust/lanzatool/src/esp.rs new file mode 100644 index 0000000..ba4f8b9 --- /dev/null +++ b/rust/lanzatool/src/esp.rs @@ -0,0 +1,22 @@ +use std::path::{Path, PathBuf}; + +pub struct EspPaths { + pub esp: PathBuf, + pub nixos: PathBuf, + pub kernel: PathBuf, + pub initrd: PathBuf, +} + +impl EspPaths { + pub fn new(esp: &str) -> Self { + let esp = Path::new(esp); + let esp_nixos = esp.join("EFI/nixos"); + + Self { + esp: esp.to_owned(), + nixos: esp_nixos.clone(), + kernel: esp_nixos.join("EFI/nixos"), + initrd: esp_nixos.join("initrd"), + } + } +} diff --git a/rust/lanzatool/src/install.rs b/rust/lanzatool/src/install.rs new file mode 100644 index 0000000..33a91a4 --- /dev/null +++ b/rust/lanzatool/src/install.rs @@ -0,0 +1,52 @@ +use std::fs; + +use std::path::Path; +use std::process::Command; + +use anyhow::Result; + +use crate::bootspec::Bootspec; +use crate::esp::EspPaths; +use crate::stub; + +pub fn install(_: &Path, bootspec: &Path, lanzaboote_bin: &Path) -> Result<()> { + let bootspec_doc: Bootspec = serde_json::from_slice(&fs::read(bootspec)?)?; + + let esp_paths = EspPaths::new(&bootspec_doc.v1.extension.esp); + + stub::assemble( + lanzaboote_bin, + &bootspec_doc.v1.extension.os_release, + &bootspec_doc.v1.kernel_params, + &esp_paths.kernel, + &esp_paths.initrd, + ) + .unwrap(); + + // Copy the files to the ESP + fs::create_dir_all(&esp_paths.nixos)?; + fs::copy(bootspec_doc.v1.kernel, esp_paths.kernel)?; + fs::copy(bootspec_doc.v1.initrd, esp_paths.initrd)?; + // install_systemd_boot(bootctl, &esp)?; + + Ok(()) +} + +fn install_systemd_boot(bootctl: &Path, esp: &Path) -> Result<()> { + let args = vec![ + String::from("install"), + String::from("--path"), + esp.display().to_string(), + ]; + + let status = Command::new(&bootctl).args(&args).status()?; + if !status.success() { + return Err(anyhow::anyhow!( + "Failed success run `{}` with args `{:?}`", + &bootctl.display(), + &args + ) + .into()); + } + Ok(()) +} diff --git a/rust/lanzatool/src/main.rs b/rust/lanzatool/src/main.rs index e09cbcc..cc00d27 100644 --- a/rust/lanzatool/src/main.rs +++ b/rust/lanzatool/src/main.rs @@ -1,5 +1,9 @@ +mod bootspec; mod cli; mod crypto; +mod esp; +mod install; +mod stub; use std::process; diff --git a/rust/lanzatool/src/stub.rs b/rust/lanzatool/src/stub.rs new file mode 100644 index 0000000..54ad410 --- /dev/null +++ b/rust/lanzatool/src/stub.rs @@ -0,0 +1,76 @@ +use std::fs; +use std::os::unix::fs::MetadataExt; +use std::path::Path; +use std::process::Command; + +use anyhow::Result; +use goblin; + +pub fn assemble( + lanzaboote_bin: &Path, + os_release: &Path, + kernel_cmdline: &[String], + kernel_path: &Path, + initrd_path: &Path, +) -> Result<()> { + // objcopy copies files into the PE binary. That's why we have to write the contents + // of some bootspec properties to disk + let kernel_cmdline_file = Path::new("/tmp/kernel_cmdline"); + fs::write(kernel_cmdline_file, kernel_cmdline.join(" "))?; + let kernel_path_file = Path::new("/tmp/kernel_path"); + fs::write(kernel_path_file, kernel_path.to_str().unwrap())?; + let initrd_path_file = Path::new("/tmp/initrd_path"); + fs::write(initrd_path_file, initrd_path.to_str().unwrap())?; + + let pe_binary = fs::read(lanzaboote_bin)?; + let pe = goblin::pe::PE::parse(&pe_binary)?; + + let os_release_offs = u64::from( + pe.sections + .iter() + .find(|s| s.name().unwrap() == ".sdmagic") + .and_then(|s| Some(s.size_of_raw_data + s.virtual_address)) + .unwrap(), + ); + + let kernel_cmdline_offs = os_release_offs + file_size(os_release)?; + let initrd_path_offs = kernel_cmdline_offs + file_size(kernel_cmdline_file)?; + let kernel_path_offs = initrd_path_offs + file_size(initrd_path_file)?; + + let args = vec![ + String::from("--add-section"), + format!(".osrel={}", path_to_string(os_release)), + String::from("--change-section-vma"), + format!(".osrel={:#x}", os_release_offs), + String::from("--add-section"), + format!(".cmdline={}", path_to_string(kernel_cmdline_file)), + String::from("--change-section-vma"), + format!(".cmdline={:#x}", kernel_cmdline_offs), + String::from("--add-section"), + format!(".initrdp={}", path_to_string(initrd_path_file)), + String::from("--change-section-vma"), + format!(".initrdp={:#x}", initrd_path_offs), + String::from("--add-section"), + format!(".kernelp={}", path_to_string(kernel_path_file)), + String::from("--change-section-vma"), + format!(".kernelp={:#x}", kernel_path_offs), + lanzaboote_bin.to_str().unwrap().to_owned(), + String::from("stub.efi"), + ]; + + let status = Command::new("objcopy").args(&args).status()?; + if !status.success() { + return Err(anyhow::anyhow!("Failed to build stub with args `{:?}`", &args).into()); + } + + Ok(()) +} + +// All Linux file paths should be convertable to strings +fn path_to_string(path: &Path) -> String { + path.to_owned().into_os_string().into_string().unwrap() +} + +fn file_size(path: &Path) -> Result { + Ok(fs::File::open(path)?.metadata()?.size()) +}