Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Netboot support #227

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
checks =
let
nixosLib = import (pkgs.path + "/nixos/lib") { };
evalConfig = import (pkgs.path + "/nixos/lib/eval-config.nix");
runTest = module: nixosLib.runTest {
imports = [ module ];
hostPkgs = pkgs;
Expand All @@ -211,7 +212,7 @@
toolFmt = toolCrane.rustfmt;
stubFmt = stubCrane.rustfmt;
} // (import ./nix/tests/lanzaboote.nix {
inherit pkgs;
inherit pkgs evalConfig;
lanzabooteModule = self.nixosModules.lanzaboote;
}) // (import ./nix/tests/stub.nix {
inherit pkgs runTest;
Expand Down
60 changes: 59 additions & 1 deletion nix/tests/lanzaboote.nix
Original file line number Diff line number Diff line change
@@ -1,13 +1,48 @@
{ pkgs
, lanzabooteModule
, evalConfig
}:

let
inherit (pkgs) lib system;
defaultTimeout = 5 * 60; # = 5 minutes

mkSecureBootTest = { name, machine ? { }, useSecureBoot ? true, useTPM2 ? false, readEfiVariables ? false, testScript }:
mkSecureBootTest = { name, machine ? { }, useSecureBoot ? true, useTPM2 ? false, readEfiVariables ? false, useNetboot ? false, testScript }:
let
lanzabooteNetbootSystem = (evalConfig {
inherit system;
modules =
[
"${pkgs.path}/nixos/modules/installer/netboot/netboot.nix"
"${pkgs.path}/nixos/modules/testing/test-instrumentation.nix"
lanzabooteModule
({ config, pkgs, ... }: {
# We refer to our own NixOS module package rather than pkgs.lzbt
# which does not exist in general.
# As the flake.nix defines the package in an ad-hoc fashion
# rather than using overlays which may not propagate here I guess?
system.build.netbootStub = pkgs.runCommand "build-netboot-stub" { } ''
mkdir -p $out
${config.boot.lanzaboote.package}/bin/lzbt \
build \
--public-key ${./fixtures/uefi-keys/keys/db/db.pem} \
--private-key ${./fixtures/uefi-keys/keys/db/db.key} \
--initrd ${config.system.build.netbootRamdisk}/initrd \
${config.system.build.toplevel} > $out/netlanzaboote.efi
'';
})
];
}).config.system;
lanzabooteNetbootTree = pkgs.symlinkJoin {
name = "ipxeBootDir";
paths = [
lanzabooteNetbootSystem.build.netbootRamdisk
lanzabooteNetbootSystem.build.kernel
# Lanzaboote stub for netboot purposes
lanzabooteNetbootSystem.build.netbootStub
];
};
lanzabooteNetbootFile = "netlanzaboote.efi";
tpmSocketPath = "/tmp/swtpm-sock";
tpmDeviceModels = {
x86_64-linux = "tpm-tis";
Expand Down Expand Up @@ -77,6 +112,10 @@ let
def swtpm_running():
tpm.check()
'';
netbootNetworkOptions = ''
import os
os.environ['QEMU_NET_OPTS'] = ','.join(os.environ.get('QEMU_NET_OPTS', "").split(',') + ["tftp=${lanzabooteNetbootTree}", "bootfile=/${lanzabooteNetbootFile}"])
'';
in
pkgs.nixosTest {
inherit name;
Expand All @@ -85,6 +124,7 @@ let
testScript = ''
${lib.optionalString useTPM2 tpm2Initialization}
${lib.optionalString readEfiVariables efiVariablesHelpers}
${lib.optionalString useNetboot netbootNetworkOptions}
${testScript}
'';

Expand All @@ -97,6 +137,15 @@ let
virtualisation = {
useBootLoader = true;
useEFIBoot = true;
# We want to be fully stateless when testing netboot usecases.
diskImage = lib.mkIf useNetboot null;
# Under netbooting, we override completely the networking option
# to point the built-in TFTP/DHCP server to the right values.
qemu.networkingOptions = lib.mkIf useNetboot (lib.mkForce [
"-net nic,netdev=user.0,model=virtio"
"-netdev user,id=user.0,tftp=${lanzabooteNetbootTree},bootfile=${lanzabooteNetbootFile},\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}"
]);
memorySize = 2048;

# We actually only want to enable features in OVMF, but at
# the moment edk2 202308 is also broken. So we downgrade it
Expand Down Expand Up @@ -142,6 +191,7 @@ let
boot.loader.efi = {
canTouchEfiVariables = true;
};

boot.lanzaboote = {
enable = true;
enrollKeys = lib.mkDefault true;
Expand Down Expand Up @@ -450,4 +500,12 @@ in
'';
};

netboot-basic = mkSecureBootTest {
name = "lanzaboote-can-netboot-prebuilt-images";
useNetboot = true;
testScript = ''
machine.start()
assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status")
'';
};
}
106 changes: 106 additions & 0 deletions rust/tool/shared/src/esp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,109 @@ pub trait EspPaths<const N: usize> {
/// Returns the path containing Linux EFI binaries
fn linux_path(&self) -> &Path;
}

pub struct BuildEspPaths {
root_path: PathBuf
}

impl EspPaths<0> for BuildEspPaths {
fn new(esp: impl AsRef<Path>) -> Self {
BuildEspPaths {
root_path: esp.as_ref().to_path_buf()
}
}

fn iter(&self) -> std::array::IntoIter<&PathBuf, 0> {
[].into_iter()
}

fn nixos_path(&self) -> &Path {
&self.root_path
}

fn linux_path(&self) -> &Path {
&self.root_path
}
}

/// Paths to the boot files of a specific generation.
pub struct EspGenerationPaths {
pub kernel: PathBuf,
pub initrd: PathBuf,
pub lanzaboote_image: PathBuf,
}

impl EspGenerationPaths {
pub fn new<const N: usize, P: EspPaths<N>>(
esp_paths: &P,
generation: &Generation,
) -> Result<Self> {
let bootspec = &generation.spec.bootspec.bootspec;

Ok(Self {
kernel: esp_paths
.nixos_path()
.join(nixos_path(&bootspec.kernel, "bzImage")?),
initrd: esp_paths.nixos_path().join(nixos_path(
bootspec
.initrd
.as_ref()
.context("Lanzaboote does not support missing initrd yet")?,
"initrd",
)?),
lanzaboote_image: esp_paths.linux_path().join(generation_path(generation)),
})
}

/// Return the used file paths to store as garbage collection roots.
pub fn to_iter(&self) -> IntoIter<&PathBuf, 3> {
[&self.kernel, &self.initrd, &self.lanzaboote_image].into_iter()
}
}

fn nixos_path(path: impl AsRef<Path>, name: &str) -> Result<PathBuf> {
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))
}

fn generation_path(generation: &Generation) -> PathBuf {
if let Some(specialisation_name) = generation.is_specialised() {
PathBuf::from(format!(
"nixos-generation-{}-specialisation-{}.efi",
generation, specialisation_name
))
} else {
PathBuf::from(format!("nixos-generation-{}.efi", generation))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn nixos_path_creates_correct_filename_from_nix_store_path() -> Result<()> {
let path =
Path::new("/nix/store/xqplddjjjy1lhzyzbcv4dza11ccpcfds-initrd-linux-6.1.1/initrd");

let generated_filename = nixos_path(path, "initrd")?;

let expected_filename =
PathBuf::from("xqplddjjjy1lhzyzbcv4dza11ccpcfds-initrd-linux-6.1.1-initrd.efi");

assert_eq!(generated_filename, expected_filename);
Ok(())
}
}
10 changes: 10 additions & 0 deletions rust/tool/shared/src/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ pub struct Generation {
}

impl Generation {
pub fn from_toplevel(toplevel: &Path, version: u64) -> Result<Self> {
let link = GenerationLink {
version,
build_time: read_build_time(toplevel).ok(),
path: toplevel.to_path_buf()
};

Self::from_link(&link)
}

pub fn from_link(link: &GenerationLink) -> Result<Self> {
let bootspec_path = link.path.join("boot.json");
let boot_json: BootJson = fs::read(bootspec_path)
Expand Down
10 changes: 10 additions & 0 deletions rust/tool/shared/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,13 @@ pub fn file_hash(file: &Path) -> Result<Hash> {
format!("Failed to read file to hash: {file:?}")
})?))
}

pub fn assemble_kernel_cmdline(init: &Path, kernel_params: Vec<String>) -> Vec<String> {
let init_string = String::from(
init.to_str()
.expect("Failed to convert init path to string"),
);
let mut kernel_cmdline: Vec<String> = vec![format!("init={}", init_string)];
kernel_cmdline.extend(kernel_params);
kernel_cmdline
}
69 changes: 68 additions & 1 deletion rust/tool/systemd/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::path::PathBuf;
use std::{path::PathBuf, io::Write};

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tempfile::TempDir;

use crate::install;
use lanzaboote_tool::architecture::Architecture;
use lanzaboote_tool::signature::KeyPair;
use lanzaboote_tool::generation::{GenerationLink, Generation}, pe, os_release::OsRelease, utils::{SecureTempDirExt, assemble_kernel_cmdline}, esp::{EspGenerationPaths, BuildEspPaths, EspPaths}};

/// The default log level.
///
Expand All @@ -27,6 +29,25 @@ pub struct Cli {
#[derive(Subcommand)]
enum Commands {
Install(InstallCommand),
Build(BuildCommand)
}

#[derive(Parser)]
struct BuildCommand {
/// sbsign Public Key
#[arg(long)]
public_key: PathBuf,

/// sbsign Private Key
#[arg(long)]
private_key: PathBuf,

/// Override initrd
#[arg(long)]
initrd: PathBuf,

/// Generation
generation: PathBuf
}

#[derive(Parser)]
Expand Down Expand Up @@ -83,6 +104,7 @@ impl Commands {
pub fn call(self) -> Result<()> {
match self {
Commands::Install(args) => install(args),
Commands::Build(args) => build(args),
}
}
}
Expand All @@ -105,3 +127,48 @@ fn install(args: InstallCommand) -> Result<()> {
)
.install()
}

fn build(args: BuildCommand) -> Result<()> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to make this simple but failed because install is very focused on install semantics
and hard to generalize.

With #204, I imagine I can simplify a lot all this stuff.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#204 went in. Looking forward to the simpler version. :)

let lanzaboote_stub =
PathBuf::from(std::env::var("LANZABOOTE_STUB").context("Failed to read LANZABOOTE_STUB env variable")?);

let key_pair = KeyPair::new(&args.public_key, &args.private_key);

let generation = Generation::from_toplevel(&args.generation, 1)
.with_context(|| format!("Failed to build generation from link: {0:?}", args.generation))?;
let bootspec = &generation.spec.bootspec.bootspec;

let tempdir = TempDir::new().context("Failed to create temporary directory")?;
let os_release = OsRelease::from_generation(&generation)
.context("Failed to build OsRelease from generation.")?;
let os_release_path = tempdir
.write_secure_file(os_release.to_string().as_bytes())
.context("Failed to write os-release file.")?;
let kernel_cmdline =
assemble_kernel_cmdline(&bootspec.init, bootspec.kernel_params.clone());
let esp = PathBuf::from("/");
let esp_paths = BuildEspPaths::new("/");
let esp_gen_paths = EspGenerationPaths::new(&esp_paths, &generation)?;

let lzbt_stub = pe::lanzaboote_image(
&tempdir,
&lanzaboote_stub,
&os_release_path,
&kernel_cmdline,
&bootspec.kernel,
&args.initrd,
&esp_gen_paths,
&esp
)?;

// Sign the stub.
let to = tempdir.path().join("signed-lzbt-stub.efi");
key_pair
.sign_and_copy(&lzbt_stub, &to)
.with_context(|| format!("Failed to copy and sign file {lzbt_stub:?} to {to:?}"))?;

// Output the stub on stdout.
std::io::stdout().write_all(&std::fs::read(to)?)?;

Ok(())
}
12 changes: 1 addition & 11 deletions rust/tool/systemd/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use lanzaboote_tool::generation::{Generation, GenerationLink};
use lanzaboote_tool::os_release::OsRelease;
use lanzaboote_tool::pe;
use lanzaboote_tool::signature::KeyPair;
use lanzaboote_tool::utils::{file_hash, SecureTempDirExt};
use lanzaboote_tool::utils::{file_hash, SecureTempDirExt, assemble_kernel_cmdline};

pub struct Installer {
broken_gens: BTreeSet<u64>,
Expand Down Expand Up @@ -453,16 +453,6 @@ pub fn append_initrd_secrets(
Ok(())
}

fn assemble_kernel_cmdline(init: &Path, kernel_params: Vec<String>) -> Vec<String> {
let init_string = String::from(
init.to_str()
.expect("Failed to convert init path to string"),
);
let mut kernel_cmdline: Vec<String> = vec![format!("init={}", init_string)];
kernel_cmdline.extend(kernel_params);
kernel_cmdline
}

/// Atomically copy a file.
///
/// First, the content is written to a temporary file (with a `.tmp` extension).
Expand Down
Loading