Merge pull request #16 from blitz/lanzatool-bootspec-from-generation

Lanzatool read bootspec for each generation
This commit is contained in:
nikstur 2022-11-26 23:13:32 +01:00 committed by GitHub
commit f080c010e9
8 changed files with 89 additions and 77 deletions

View File

@ -140,8 +140,7 @@
import json import json
import os.path import os.path
bootspec = None bootspec = None
def extract_bspec_attr(attr):
return bootspec.get(attr)
def convert_to_esp(store_file_path): def convert_to_esp(store_file_path):
store_dir = os.path.basename(os.path.dirname(store_file_path)) store_dir = os.path.basename(os.path.dirname(store_file_path))
filename = os.path.basename(store_file_path) filename = os.path.basename(store_file_path)
@ -192,8 +191,8 @@
}; };
boot.initrd.preDeviceCommands = '' boot.initrd.preDeviceCommands = ''
grep "this is a very secure secret" /etc/iamasecret grep "this is a very secure secret" /etc/iamasecret
''; '';
}; };
testScript = '' testScript = ''
machine.start() machine.start()
@ -203,18 +202,17 @@
is-initrd-secured = mkUnsignedTest { is-initrd-secured = mkUnsignedTest {
name = "unsigned-initrd-do-not-boot-under-secureboot"; name = "unsigned-initrd-do-not-boot-under-secureboot";
path = { path = {
src = "extract_bspec_attr('initrd')"; src = "bootspec.get('initrd')";
dst = "convert_to_esp(extract_bspec_attr('initrd'))"; dst = "convert_to_esp(bootspec.get('initrd'))";
}; };
}; };
is-kernel-secured = mkUnsignedTest { is-kernel-secured = mkUnsignedTest {
name = "unsigned-kernel-do-not-boot-under-secureboot"; name = "unsigned-kernel-do-not-boot-under-secureboot";
path = { path = {
src = "extract_bspec_attr('kernel')"; src = "bootspec.get('kernel')";
dst = "convert_to_esp(extract_bspec_attr('kernel'))"; dst = "convert_to_esp(bootspec.get('kernel'))";
}; };
}; };
};
};
}; };
} }

View File

@ -38,16 +38,20 @@ in
boot.loader.supportsInitrdSecrets = mkForce true; boot.loader.supportsInitrdSecrets = mkForce true;
boot.loader.external = { boot.loader.external = {
enable = true; enable = true;
passBootspec = true; installHook = pkgs.writeShellScript "bootinstall" ''
installHook = "${pkgs.writeShellScriptBin "bootinstall" '' ${optionalString cfg.enrollKeys ''
${optionalString cfg.enrollKeys '' mkdir -p /tmp/pki
mkdir -p /tmp/pki cp -r ${cfg.pkiBundle}/* /tmp/pki
cp -r ${cfg.pkiBundle}/* /tmp/pki ${sbctlWithPki}/bin/sbctl enroll-keys --yes-this-might-brick-my-machine
${sbctlWithPki}/bin/sbctl enroll-keys --yes-this-might-brick-my-machine ''}
''}
${cfg.package}/bin/lanzatool install --pki-bundle ${cfg.pkiBundle} --public-key ${cfg.publicKeyFile} --private-key ${cfg.privateKeyFile} "$@" /nix/var/nix/profiles/system-*-link ${cfg.package}/bin/lanzatool install \
''}/bin/bootinstall"; --pki-bundle ${cfg.pkiBundle} \
# ${cfg.package}/bin/lanzatool install ${optionalString cfg.enrollKeys "--auto-enroll"} --pki-bundle ${cfg.pkiBundle} --public-key ${cfg.publicKeyFile} \
--private-key ${cfg.privateKeyFile} \
${config.boot.loader.efi.efiSysMountPoint} \
/nix/var/nix/profiles/system-*-link
'';
}; };
}; };
} }

View File

@ -15,9 +15,6 @@
"label": "LanzaOS", "label": "LanzaOS",
"toplevel": "/run/current-system", "toplevel": "/run/current-system",
"extension": { "extension": {
"esp": "esp",
"systemd": "/run/current-system/systemd",
"bootctl": "/run/current-system/sw/bin/bootctl",
"osRelease": "/etc/os-release" "osRelease": "/etc/os-release"
} }
} }

View File

@ -25,8 +25,5 @@ pub struct Bootspec {
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Extension { pub struct Extension {
pub esp: String,
pub bootctl: PathBuf,
pub os_release: PathBuf, pub os_release: PathBuf,
pub systemd: PathBuf,
} }

View File

@ -35,7 +35,7 @@ struct InstallCommand {
#[arg(long, default_value = "false")] #[arg(long, default_value = "false")]
auto_enroll: bool, auto_enroll: bool,
bootspec: PathBuf, esp: PathBuf,
generations: Vec<PathBuf>, generations: Vec<PathBuf>,
} }
@ -67,7 +67,7 @@ fn install(args: InstallCommand) -> Result<()> {
args.private_key, args.private_key,
args.pki_bundle, args.pki_bundle,
args.auto_enroll, args.auto_enroll,
args.bootspec, args.esp,
args.generations, args.generations,
) )
.install() .install()

View File

@ -1,7 +1,6 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::bootspec::Bootspec;
use crate::generation::Generation; use crate::generation::Generation;
pub struct EspPaths { pub struct EspPaths {
@ -18,20 +17,22 @@ pub struct EspPaths {
} }
impl EspPaths { impl EspPaths {
pub fn new(esp: &str, generation: Generation, bootspec: &Bootspec) -> Result<Self> { pub fn new(esp: impl AsRef<Path>, generation: &Generation) -> Result<Self> {
let esp = Path::new(esp); let esp = esp.as_ref();
let esp_nixos = esp.join("EFI/nixos"); let esp_nixos = esp.join("EFI/nixos");
let esp_linux = esp.join("EFI/Linux"); let esp_linux = esp.join("EFI/Linux");
let esp_systemd = esp.join("EFI/systemd"); let esp_systemd = esp.join("EFI/systemd");
let esp_efi_fallback_dir = esp.join("EFI/BOOT"); let esp_efi_fallback_dir = esp.join("EFI/BOOT");
let bootspec = &generation.bootspec;
Ok(Self { Ok(Self {
esp: esp.to_owned(), esp: esp.to_path_buf(),
nixos: esp_nixos.clone(), nixos: esp_nixos.clone(),
kernel: esp_nixos.join(nixos_path(&bootspec.kernel, "bzImage")?), kernel: esp_nixos.join(nixos_path(&bootspec.kernel, "bzImage")?),
initrd: esp_nixos.join(nixos_path(&bootspec.initrd, "initrd")?), initrd: esp_nixos.join(nixos_path(&bootspec.initrd, "initrd")?),
linux: esp_linux.clone(), linux: esp_linux.clone(),
lanzaboote_image: esp_linux.join(generation_path(generation)), lanzaboote_image: esp_linux.join(generation_path(&generation)),
efi_fallback_dir: esp_efi_fallback_dir.clone(), efi_fallback_dir: esp_efi_fallback_dir.clone(),
efi_fallback: esp_efi_fallback_dir.join("BOOTX64.EFI"), efi_fallback: esp_efi_fallback_dir.join("BOOTX64.EFI"),
systemd: esp_systemd.clone(), systemd: esp_systemd.clone(),
@ -60,6 +61,6 @@ fn nixos_path(path: impl AsRef<Path>, name: &str) -> Result<PathBuf> {
Ok(PathBuf::from(nixos_filename)) Ok(PathBuf::from(nixos_filename))
} }
fn generation_path(generation: Generation) -> PathBuf { fn generation_path(generation: &Generation) -> PathBuf {
PathBuf::from(format!("nixos-generation-{}.efi", generation)) PathBuf::from(format!("nixos-generation-{}.efi", generation))
} }

View File

@ -1,36 +1,55 @@
use std::fmt; use std::fmt;
use std::fs;
use std::path::Path; use std::path::Path;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use crate::bootspec::Bootspec;
#[derive(Debug)] #[derive(Debug)]
pub struct Generation(u64); pub struct Generation {
version: u64,
pub bootspec: Bootspec,
}
impl Generation { impl Generation {
pub fn from_toplevel(toplevel: impl AsRef<Path>) -> Result<Self> { pub fn from_toplevel(toplevel: impl AsRef<Path>) -> Result<Self> {
let file_name = toplevel.as_ref().file_name().ok_or(anyhow::anyhow!( let bootspec_path = toplevel.as_ref().join("bootspec/boot.v1.json");
"Failed to extract file name from generation" let bootspec: Bootspec = serde_json::from_slice(
))?; &fs::read(&bootspec_path).context("Failed to read bootspec file")?,
)
.context("Failed to parse bootspec json")?;
let file_name_str = file_name Ok(Self {
.to_str() version: parse_version(toplevel)?,
.with_context(|| "Failed to convert file name of generation to string")?; bootspec,
})
let generation_version = file_name_str
.split("-")
.nth(1)
.ok_or(anyhow::anyhow!("Failed to extract version from generation"))?;
let parsed_generation_version = generation_version.parse().with_context(|| {
format!("Failed to parse generation version: {}", generation_version)
})?;
Ok(Self(parsed_generation_version))
} }
} }
impl fmt::Display for Generation { impl fmt::Display for Generation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0) write!(f, "{}", self.version)
} }
} }
fn parse_version(toplevel: impl AsRef<Path>) -> Result<u64> {
let file_name = toplevel.as_ref().file_name().ok_or(anyhow::anyhow!(
"Failed to extract file name from generation"
))?;
let file_name_str = file_name
.to_str()
.with_context(|| "Failed to convert file name of generation to string")?;
let generation_version = file_name_str
.split("-")
.nth(1)
.ok_or(anyhow::anyhow!("Failed to extract version from generation"))?;
let parsed_generation_version = generation_version
.parse()
.with_context(|| format!("Failed to parse generation version: {}", generation_version))?;
Ok(parsed_generation_version)
}

View File

@ -7,7 +7,6 @@ use anyhow::{Context, Result};
use nix::unistd::sync; use nix::unistd::sync;
use tempfile::tempdir; use tempfile::tempdir;
use crate::bootspec::Bootspec;
use crate::esp::EspPaths; use crate::esp::EspPaths;
use crate::generation::Generation; use crate::generation::Generation;
use crate::pe; use crate::pe;
@ -20,7 +19,7 @@ pub struct Installer {
private_key: PathBuf, private_key: PathBuf,
_pki_bundle: Option<PathBuf>, _pki_bundle: Option<PathBuf>,
_auto_enroll: bool, _auto_enroll: bool,
bootspec: PathBuf, esp: PathBuf,
generations: Vec<PathBuf>, generations: Vec<PathBuf>,
} }
@ -32,7 +31,7 @@ impl Installer {
private_key: PathBuf, private_key: PathBuf,
_pki_bundle: Option<PathBuf>, _pki_bundle: Option<PathBuf>,
_auto_enroll: bool, _auto_enroll: bool,
bootspec: PathBuf, esp: PathBuf,
generations: Vec<PathBuf>, generations: Vec<PathBuf>,
) -> Self { ) -> Self {
Self { Self {
@ -42,7 +41,7 @@ impl Installer {
private_key, private_key,
_pki_bundle, _pki_bundle,
_auto_enroll, _auto_enroll,
bootspec, esp,
generations, generations,
} }
} }
@ -50,7 +49,7 @@ impl Installer {
pub fn install(&self) -> Result<()> { pub fn install(&self) -> Result<()> {
for toplevel in &self.generations { for toplevel in &self.generations {
let generation = Generation::from_toplevel(toplevel).with_context(|| { let generation = Generation::from_toplevel(toplevel).with_context(|| {
format!("Failed to extract generation version from: {toplevel:?}") format!("Failed to build generation from toplevel: {toplevel:?}")
})?; })?;
println!("Installing generation {generation}"); println!("Installing generation {generation}");
@ -64,16 +63,14 @@ impl Installer {
pub fn install_generation(&self, generation: Generation) -> Result<()> { pub fn install_generation(&self, generation: Generation) -> Result<()> {
println!("Reading bootspec..."); println!("Reading bootspec...");
let bootspec_doc: Bootspec = serde_json::from_slice( let bootspec = &generation.bootspec;
&fs::read(&self.bootspec).context("Failed to read bootspec file")?,
)
.context("Failed to parse bootspec json")?;
let esp_paths = EspPaths::new(&bootspec_doc.extension.esp, generation, &bootspec_doc)?; let esp_paths = EspPaths::new(&self.esp, &generation)?;
println!("Assembling lanzaboote image..."); println!("Assembling lanzaboote image...");
let kernel_cmdline = assemble_kernel_cmdline(bootspec_doc.init, bootspec_doc.kernel_params); let kernel_cmdline =
assemble_kernel_cmdline(&bootspec.init, bootspec.kernel_params.clone());
// prepare a secure temporary directory // prepare a secure temporary directory
// permission bits are not set, because when files below // permission bits are not set, because when files below
@ -86,7 +83,7 @@ impl Installer {
let lanzaboote_image = pe::lanzaboote_image( let lanzaboote_image = pe::lanzaboote_image(
&secure_temp_dir, &secure_temp_dir,
&self.lanzaboote_stub, &self.lanzaboote_stub,
&bootspec_doc.extension.os_release, &bootspec.extension.os_release,
&kernel_cmdline, &kernel_cmdline,
&esp_paths.kernel, &esp_paths.kernel,
&esp_paths.initrd, &esp_paths.initrd,
@ -97,8 +94,8 @@ impl Installer {
println!("Wrapping initrd into a PE binary..."); println!("Wrapping initrd into a PE binary...");
let initrd_location = secure_temp_dir.path().join("initrd"); let initrd_location = secure_temp_dir.path().join("initrd");
copy(&bootspec_doc.initrd, &initrd_location)?; copy(&bootspec.initrd, &initrd_location)?;
if let Some(initrd_secrets_script) = bootspec_doc.initrd_secrets { if let Some(initrd_secrets_script) = &bootspec.initrd_secrets {
append_initrd_secrets(&initrd_secrets_script, &initrd_location)?; append_initrd_secrets(&initrd_secrets_script, &initrd_location)?;
} }
let wrapped_initrd = pe::wrap_initrd(&secure_temp_dir, &self.initrd_stub, &initrd_location) let wrapped_initrd = pe::wrap_initrd(&secure_temp_dir, &self.initrd_stub, &initrd_location)
@ -108,16 +105,15 @@ impl Installer {
let signer = Signer::new(&self.public_key, &self.private_key); let signer = Signer::new(&self.public_key, &self.private_key);
let systemd_boot = bootspec_doc let systemd_boot = bootspec
.extension .toplevel
.systemd .join("systemd/lib/systemd/boot/efi/systemd-bootx64.efi");
.join("lib/systemd/boot/efi/systemd-bootx64.efi");
let files_to_copy_and_sign = [ let files_to_copy_and_sign = [
(&systemd_boot, &esp_paths.efi_fallback), (&systemd_boot, &esp_paths.efi_fallback),
(&systemd_boot, &esp_paths.systemd_boot), (&systemd_boot, &esp_paths.systemd_boot),
(&lanzaboote_image, &esp_paths.lanzaboote_image), (&lanzaboote_image, &esp_paths.lanzaboote_image),
(&bootspec_doc.kernel, &esp_paths.kernel), (&bootspec.kernel, &esp_paths.kernel),
(&wrapped_initrd, &esp_paths.initrd), (&wrapped_initrd, &esp_paths.initrd),
]; ];
@ -160,11 +156,11 @@ pub fn append_initrd_secrets(
Ok(()) Ok(())
} }
fn assemble_kernel_cmdline(init: PathBuf, kernel_params: Vec<String>) -> Vec<String> { fn assemble_kernel_cmdline(init: &Path, kernel_params: Vec<String>) -> Vec<String> {
let init_string = init let init_string = String::from(
.into_os_string() init.to_str()
.into_string() .expect("Failed to convert init path to string"),
.expect("Failed to convert init path to string"); );
let mut kernel_cmdline: Vec<String> = vec![format!("init={}", init_string)]; let mut kernel_cmdline: Vec<String> = vec![format!("init={}", init_string)];
kernel_cmdline.extend(kernel_params); kernel_cmdline.extend(kernel_params);
kernel_cmdline kernel_cmdline