Skip to content

Commit

Permalink
Merge pull request #303 from nix-community/refactor-signature
Browse files Browse the repository at this point in the history
shared: generalize signature schemes
  • Loading branch information
nikstur authored Sep 3, 2024
2 parents f5a3a7d + 8373fae commit e7bd94e
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 156 deletions.
7 changes: 5 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@
, src
, target ? null
, doCheck ? true
# By default, it builds the default members of the workspace.
, packages ? null
, extraArgs ? { }
}:
let
Expand Down Expand Up @@ -117,7 +119,9 @@
#[cfg_attr(any(target_os = "none", target_os = "uefi"), export_name = "efi_main")]
fn main() {}
'';
} // extraArgs;

cargoExtraArgs = (extraArgs.cargoExtraArgs or "") + (if packages != null then (lib.concatStringsSep " " (map (p: "--package ${p}") packages)) else "");
} // builtins.removeAttrs extraArgs [ "cargoExtraArgs" ];

cargoArtifacts = craneLib.buildDepsOnly commonArgs;
in
Expand Down Expand Up @@ -203,7 +207,6 @@
inherit (self.nixosModules) lanzaboote uki;
};
});

devShells.default = pkgs.mkShell {
shellHook = ''
${config.pre-commit.installationScript}
Expand Down
2 changes: 1 addition & 1 deletion rust/tool/shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ edition.workspace = true
[dependencies]
anyhow = "1"
goblin = "0.7"
serde = "1"
serde_json = "1"
tempfile = "3.10.1"
bootspec = "1"
Expand All @@ -19,3 +18,4 @@ sha2 = "0.10"
# different versions.
fastrand = "2.0.2"
log = { version = "0.4", features = ["std"] }
serde = { version = "1.0.194", features = ["derive"] }
111 changes: 92 additions & 19 deletions rust/tool/shared/src/pe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,108 @@ use std::process::Command;

use anyhow::{Context, Result};
use goblin::pe::PE;
use serde::{Deserialize, Serialize};
use tempfile::TempDir;

use crate::utils::{file_hash, tmpname, SecureTempDirExt};

#[derive(Debug, Serialize, Deserialize)]
pub struct StubParameters {
pub lanzaboote_store_path: PathBuf,
pub kernel_cmdline: Vec<String>,
pub os_release_contents: Vec<u8>,
pub kernel_store_path: PathBuf,
pub initrd_store_path: PathBuf,
/// Kernel path rooted at the ESP
/// i.e. if you refer to /boot/efi/EFI/NixOS/kernel.efi
/// this gets turned into \\EFI\\NixOS\\kernel.efi as a UTF-16 string
/// at assembling time.
pub kernel_path_at_esp: String,
/// Same as kernel.
pub initrd_path_at_esp: String,
}

impl StubParameters {
pub fn new(
lanzaboote_stub: &Path,
kernel_path: &Path,
initrd_path: &Path,
kernel_target: &Path,
initrd_target: &Path,
esp: &Path,
) -> Result<Self> {
// Resolve maximally those paths
// We won't verify they are store paths, otherwise the mocking strategy will fail for our
// unit tests.

Ok(Self {
lanzaboote_store_path: lanzaboote_stub.to_path_buf(),
kernel_store_path: kernel_path.to_path_buf(),
initrd_store_path: initrd_path.to_path_buf(),
kernel_path_at_esp: esp_relative_uefi_path(esp, kernel_target)?,
initrd_path_at_esp: esp_relative_uefi_path(esp, initrd_target)?,
kernel_cmdline: Vec::new(),
os_release_contents: Vec::new(),
})
}

pub fn with_os_release_contents(mut self, os_release_contents: &[u8]) -> Self {
self.os_release_contents = os_release_contents.to_vec();
self
}

pub fn with_cmdline(mut self, cmdline: &[String]) -> Self {
self.kernel_cmdline = cmdline.to_vec();
self
}
}

/// Performs the evil operation
/// of calling the appender script to append
/// initrd "secrets" (not really) to the initrd.
pub fn append_initrd_secrets(
append_initrd_secrets_path: &Path,
initrd_path: &PathBuf,
generation_version: u64,
) -> Result<()> {
let status = Command::new(append_initrd_secrets_path)
.args(vec![initrd_path])
.status()
.context("Failed to append initrd secrets")?;
if !status.success() {
return Err(anyhow::anyhow!(
"Failed to append initrd secrets for generation {} with args `{:?}`",
generation_version,
vec![append_initrd_secrets_path, initrd_path]
));
}

Ok(())
}

/// Assemble a lanzaboote image.
#[allow(clippy::too_many_arguments)]
pub fn lanzaboote_image(
// Because the returned path of this function is inside the tempdir as well, the tempdir must
// live longer than the function. This is why it cannot be created inside the function.
tempdir: &TempDir,
lanzaboote_stub: &Path,
os_release: &Path,
kernel_cmdline: &[String],
kernel_source: &Path,
kernel_target: &Path,
initrd_source: &Path,
initrd_target: &Path,
esp: &Path,
stub_parameters: &StubParameters,
) -> Result<PathBuf> {
// objcopy can only copy files into the PE binary. That's why we
// have to write the contents of some bootspec properties to disk.
let kernel_cmdline_file = tempdir.write_secure_file(kernel_cmdline.join(" "))?;
let kernel_cmdline_file =
tempdir.write_secure_file(stub_parameters.kernel_cmdline.join(" "))?;

let kernel_path_file =
tempdir.write_secure_file(esp_relative_uefi_path(esp, kernel_target)?)?;
let kernel_hash_file = tempdir.write_secure_file(file_hash(kernel_source)?.as_slice())?;
let kernel_path_file = tempdir.write_secure_file(&stub_parameters.kernel_path_at_esp)?;
let kernel_hash_file =
tempdir.write_secure_file(file_hash(&stub_parameters.kernel_store_path)?.as_slice())?;

let initrd_path_file =
tempdir.write_secure_file(esp_relative_uefi_path(esp, initrd_target)?)?;
let initrd_hash_file = tempdir.write_secure_file(file_hash(initrd_source)?.as_slice())?;
let initrd_path_file = tempdir.write_secure_file(&stub_parameters.initrd_path_at_esp)?;
let initrd_hash_file =
tempdir.write_secure_file(file_hash(&stub_parameters.initrd_store_path)?.as_slice())?;

let os_release_offs = stub_offset(lanzaboote_stub)?;
let kernel_cmdline_offs = os_release_offs + file_size(os_release)?;
let os_release = tempdir.write_secure_file(&stub_parameters.os_release_contents)?;
let os_release_offs = stub_offset(&stub_parameters.lanzaboote_store_path)?;
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 initrd_hash_offs = kernel_path_offs + file_size(&kernel_path_file)?;
Expand All @@ -54,7 +123,11 @@ pub fn lanzaboote_image(
];

let image_path = tempdir.path().join(tmpname());
wrap_in_pe(lanzaboote_stub, sections, &image_path)?;
wrap_in_pe(
&stub_parameters.lanzaboote_store_path,
sections,
&image_path,
)?;
Ok(image_path)
}

Expand Down
70 changes: 0 additions & 70 deletions rust/tool/shared/src/signature.rs

This file was deleted.

118 changes: 118 additions & 0 deletions rust/tool/shared/src/signature/local.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use crate::pe::lanzaboote_image;
use crate::utils::SecureTempDirExt;
use std::ffi::OsString;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{Context, Result};
use tempfile::tempdir;

use super::Signer;

/// A local keypair is a signer that reuses private key material
/// on the disk.
///
/// The security of the private key is the responsibility of the user.
///
/// Currently, signature happens via `sbsign` where the input is temporarily
/// copied in a secure directory and signed over there.
///
/// In the future, `sbsign` may be removed to perform signature in-memory
/// without any temporary directory.
#[derive(Debug, Clone)]
pub struct LocalKeyPair {
pub private_key: PathBuf,
pub public_key: PathBuf,
}

impl LocalKeyPair {
pub fn new(public_key: &Path, private_key: &Path) -> Self {
Self {
public_key: public_key.into(),
private_key: private_key.into(),
}
}
}

impl Signer for LocalKeyPair {
fn get_public_key(&self) -> Result<Vec<u8>> {
Ok(std::fs::read(&self.public_key)?)
}

fn sign_and_copy(&self, from: &Path, to: &Path) -> Result<()> {
let args: Vec<OsString> = vec![
OsString::from("--key"),
self.private_key.clone().into(),
OsString::from("--cert"),
self.public_key.clone().into(),
from.as_os_str().to_owned(),
OsString::from("--output"),
to.as_os_str().to_owned(),
];

let output = Command::new("sbsign")
.args(&args)
.output()
.context("Failed to run sbsign. Most likely, the binary is not on PATH.")?;

if !output.status.success() {
std::io::stderr()
.write_all(&output.stderr)
.context("Failed to write output of sbsign to stderr.")?;
log::debug!("sbsign failed with args: `{args:?}`.");
return Err(anyhow::anyhow!("Failed to sign {to:?}."));
}

Ok(())
}

fn sign_store_path(&self, store_path: &Path) -> Result<Vec<u8>> {
let working_tree = tempdir()?;
let to = &working_tree.path().join("signed.efi");
self.sign_and_copy(store_path, to)?;

Ok(std::fs::read(to)?)
}

fn build_and_sign_stub(&self, stub: &crate::pe::StubParameters) -> Result<Vec<u8>> {
let working_tree = tempdir()?;
let lzbt_image_path =
lanzaboote_image(&working_tree, stub).context("Failed to build a lanzaboote image")?;
let to = working_tree.path().join("signed-stub.efi");
self.sign_and_copy(&lzbt_image_path, &to)?;

std::fs::read(&to).context("Failed to read a lanzaboote image")
}

fn verify(&self, pe_binary: &[u8]) -> Result<bool> {
let working_tree = tempdir().context("Failed to get a temporary working tree")?;
let from = working_tree
.write_secure_file(pe_binary)
.context("Failed to write the PE binary in a secure file for verification")?;

self.verify_path(&from)
}

fn verify_path(&self, path: &Path) -> Result<bool> {
let args: Vec<OsString> = vec![
OsString::from("--cert"),
self.public_key.clone().into(),
path.as_os_str().to_owned(),
];

let output = Command::new("sbverify")
.args(&args)
.output()
.context("Failed to run sbverify. Most likely, the binary is not on PATH.")?;

if !output.status.success() {
if std::io::stderr().write_all(&output.stderr).is_err() {
return Ok(false);
};
log::debug!("sbverify failed with args: `{args:?}`.");
return Ok(false);
}
Ok(true)
}
}
Loading

0 comments on commit e7bd94e

Please sign in to comment.