rust: Add a new tool called 'pvimg'

Add a new tool called 'pvimg' that can be used to create and inspect
Secure Execution images. It has several subcommands:

 + create: create an IBM Secure Execution image (genprotimg compatible
	  sytnax) and C-'genprotimg' is going to be replaced by a
	  symlink to this subcommand.
 + test: test various aspects of an existing Secure Execution image
 + info: print information about an existing Secure Execution
	 image (experimental API!)
 + version: print version and exit

As mentioned above, the 'genprotimg' tool is now a symbolic link to the
'pvimg create' subcommand and the CLI is backward compatible with the
original genprotimg CLI, with the following exceptions:

  - '-v' increases the verbosity instead of showing the version
  - '-V' is now deprecated in favor of '-v'
  - an existing output file is no longer silently overwritten, but there
    is a new flag '--overwrite' to get the original behavior
  - experimental options are no longer described in the help
  - the commands '--cert ...' and '--root-ca' are now mutually exclusive
  - to '--no-verify'
  - there is now a component check, e.g. it checks if the specified
    Linux kernel looks like a raw binary s390x kernel. These checks can be
    disabled by using the new command line flag '--no-component-check'

Acked-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
This commit is contained in:
Marc Hartmayer
2024-11-21 18:26:50 +00:00
committed by Jan Höppner
parent f524b0b8dc
commit f4cf4ae6eb
27 changed files with 3332 additions and 22 deletions

View File

@@ -25,6 +25,8 @@ Package contents
Automatic configure APQNs within an SE KVM guest
- pvsecret:
Manage secrets for IBM Secure Execution guests
- pvimg:
Create and inspect IBM Secure Execution images
* dasdfmt:
Low-level format ECKD DASDs with the classical Linux disk layout or the new
@@ -304,26 +306,28 @@ HAVE_FUSE=0`".
The following table provides an overview of the used libraries and
build options:
| __LIBRARY__ | __BUILD OPTION__ | __TOOLS__ |
|----------------|:------------------:|:-------------------------------------:|
| fuse3 | `HAVE_FUSE` | cmsfs-fuse, zdsfs, hmcdrvfs, zgetdump,|
| | | hsavmcore |
| zlib | `HAVE_ZLIB` | zgetdump, dump2tar |
| ncurses | `HAVE_NCURSES` | hyptop |
| net-snmp | `HAVE_SNMP` | osasnmpd |
| glibc-static | `HAVE_LIBC_STATIC` | zfcpdump |
| openssl | `HAVE_OPENSSL` | genprotimg, zkey, libekmfweb, |
| | | libkmipclient, zgetdump, |
| | | rust/pvattest, rust/pvsecret, |
| cryptsetup | `HAVE_CRYPTSETUP2` | zkey-cryptsetup |
| json-c | `HAVE_JSONC` | zkey-cryptsetup, libekmfweb, |
| | | libkmipclient |
| glib2 | `HAVE_GLIB2` | genprotimg, zgetdump |
| libcurl | `HAVE_LIBCURL` | genprotimg, libekmfweb, libkmipclient,|
| | | rust/pvattest, rust/pvsecret, |
| libxml2 | `HAVE_LIBXML2` | libkmipclient |
| systemd | `HAVE_SYSTEMD` | hsavmcore |
| libudev | `HAVE_LIBUDEV` | cpacfstatsd |
| __LIBRARY__ | __BUILD OPTION__ | __TOOLS__ |
|--------------|:------------------:|:--------------------------------------:|
| fuse3 | `HAVE_FUSE` | cmsfs-fuse, zdsfs, hmcdrvfs, zgetdump, |
| | | hsavmcore |
| zlib | `HAVE_ZLIB` | zgetdump, dump2tar |
| ncurses | `HAVE_NCURSES` | hyptop |
| net-snmp | `HAVE_SNMP` | osasnmpd |
| glibc-static | `HAVE_LIBC_STATIC` | zfcpdump |
| openssl | `HAVE_OPENSSL` | genprotimg, zkey, libekmfweb, |
| | | libkmipclient, zgetdump, |
| | | rust/pvattest, rust/pvsecret, |
| | | rust/pvimg |
| cryptsetup | `HAVE_CRYPTSETUP2` | zkey-cryptsetup |
| json-c | `HAVE_JSONC` | zkey-cryptsetup, libekmfweb, |
| | | libkmipclient |
| glib2 | `HAVE_GLIB2` | genprotimg, zgetdump |
| libcurl | `HAVE_LIBCURL` | genprotimg, libekmfweb, libkmipclient, |
| | | rust/pvattest, rust/pvsecret, |
| | | rust/pvimg |
| libxml2 | `HAVE_LIBXML2` | libkmipclient |
| systemd | `HAVE_SYSTEMD` | hsavmcore |
| libudev | `HAVE_LIBUDEV` | cpacfstatsd |
This table lists additional build or install options:
@@ -363,6 +367,14 @@ the different tools are provided:
The runtime requirements are: openssl-libs (>= 1.1.0) and glib2.
* rust/pvimg:
For building pvimg you need OpenSSL version 1.1.1 or newer
installed (openssl-devel.rpm). Also required is cargo and libcurl.
Tip: you may skip the pvimg build by adding
`HAVE_OPENSSL=0`, `HAVE_LIBCURL=0`, or `HAVE_CARGO=0`.
The runtime requirements are: openssl-libs (>= 1.1.1) and libcurl.
* rust/pvattest:
For building pvattest you need OpenSSL version 1.1.1 or newer
installed (openssl-devel.rpm). Also required is cargo and libcurl.

View File

@@ -25,7 +25,7 @@ endif #HOSTARCH
ifneq (${HAVE_OPENSSL},0)
ifneq (${HAVE_LIBCURL},0)
PV_TARGETS := pvsecret pvattest
PV_TARGETS := pvsecret pvattest pvimg
ifeq ($(HOST_ARCH),s390x)
PV_TARGETS += pvapconfig
@@ -88,6 +88,7 @@ install: $(INSTALL_TARGETS)
$(INSTALL) -d -m 755 $(DESTDIR)$(PVIMG_PKGDATADIR)
$(INSTALL) -g $(GROUP) -o $(OWNER) -m 755 pvimg/tools/check_hostkeydoc $(DESTDIR)$(PVIMG_PKGDATADIR)
$(MAKE) -C pvimg/boot install
ln -sf pvimg $(DESTDIR)$(USRBINDIR)/genprotimg
print-rust-targets:
echo $(BUILD_TARGETS)

84
rust/pvimg/README.md Normal file
View File

@@ -0,0 +1,84 @@
# pvimg
`pvimg create` takes a kernel, key files, optionally an initrd image, optionally a
file containing the kernel command line parameters, and generates a single,
bootable image file. The generated image file consists of a concatenation of a
plain text boot loader, the encrypted components for kernel, initrd, kernel
command line, and the integrity-protected Secure Execution header, containing
the metadata necessary for running the guest in protected mode. See [Memory
Layout](#memory-layout) for details about the internal structure of the created
image.
It is possible to use the generated image as a kernel for zipl or for a direct
kernel boot using QEMU.
## Getting started
If all dependencies are met a simple `make` call in the source tree should be
enough for building `pvimg`.
## Details
The main idea of `pvimg create` is:
1. Generate all keys, IVs, and other information needed for the encryption of
the components and the generation of the PV header
2. add stub stage3a (so we can calculate the memory addresses)
3. add components: prepare the components (alignment and encryption) and add
them to the memory layout
4. build and add stage3b: generate the stage3b and add it to the memory layout
5. generate the Secure Execution header: generate the hashes (pld, ald, and tld)
of the components and create the header and IPIB
6. parameterize the stub stage3a: uses the address of the IPIB and Secure
Execution header
8. write the final image to the specified output path.
### Boot Loader
The boot loader consists of two parts:
1. stage3a boot loader (cleartext), this loader is responsible for the
transition into the protected mode by doing `diag308` subcode 8 and 10 calls.
2. stage3b boot loader (encrypted), this loader is very similar to the normal
zipl stage3 boot loader. It will be loaded by the Ultravisor after the
successful transition into protected mode. Like the zipl stage3 boot loader
it moves the kernel and patches in the values for initrd and kernel command
line.
The loaders have the following constraints:
1. It must be possible to place stage3a and stage3b at a location greater than
0x10000 because the zipl stage3 loader zeroes out everything at addresses
lower than 0x10000 of the image.
2. As the stage3 loader of zipl assumes that the passed kernel image looks like
a normal kernel image, the zipl stage3 loader modifies the content at the
memory area 0x10400 - 0x10800, therefore we leave this area unused in our
stage3a loader.
3. The default entry address used by the zipl stage3 loader is 0x10000 so we add
a simple branch to 0x11000 at 0x10000 so the zipl stage3 loader can modify
the area 0x10400 - 0x10800 without affecting the stage3a loader.
#### Stage3b
The `stage3b.bin` is linked at address 0x9000, therefore it will not work at
another address. The relocation support for the stage3b loader, so that it can
be loaded at addresses != 0x9000, is added in the loader with the name
`stage3b_reloc.bin`. By default, if we're talking about stage3b we refer to
`stage3b_reloc.bin.`
### Memory Layout
The memory layout of the bootable file looks like:
| Start | End | Use |
|--------------------------|------------|-----------------------------------------------------------------------|
| 0 | 0x7 | Short PSW, starting instruction at 0x11000 |
| 0x10000 | 0x10012 | Branch to 0x11000 |
| 0x10013 | 0x10fff | Left intentionally unused |
| 0x11000 | 0x12fff | Stage3a |
| 0x14000 | 0x1[45]fff | SE-header used for the diag308 call (size can be either 1 or 2 pages) |
| `NEXT_PAGE_ALIGNED_ADDR` | | Encrypted kernel |
| `NEXT_PAGE_ALIGNED_ADDR` | | Encrypted kernel parameters |
| `NEXT_PAGE_ALIGNED_ADDR` | | Encrypted initrd |
| `NEXT_PAGE_ALIGNED_ADDR` | | Encrypted stage3b_reloc |
| `NEXT_PAGE_ALIGNED_ADDR` | | IPIB used as argument for the diag308 call |

29
rust/pvimg/build.rs Normal file
View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
// it under the terms of the MIT license. See LICENSE for details.
#![allow(missing_docs)]
use std::io::Error;
use clap_complete::{generate_to, Shell};
include!("src/cli.rs");
fn main() -> Result<(), Error> {
let outdir = env::var_os("OUT_DIR").unwrap();
let crate_name = env!("CARGO_PKG_NAME");
for &shell in Shell::value_variants() {
for (name, mut cmd) in [
(crate_name, CliOptions::command()),
("genprotimg", GenprotimgCliOptions::command()),
] {
generate_to(shell, &mut cmd, name, &outdir)?;
}
}
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/cli.rs");
println!("cargo:rerun-if-changed=../utils/src/cli.rs");
Ok(())
}

752
rust/pvimg/src/cli.rs Normal file
View File

@@ -0,0 +1,752 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::{env, fmt::Display, path::PathBuf};
use clap::{ArgGroup, Args, Command, CommandFactory, Parser, ValueEnum, ValueHint};
use log::warn;
use utils::{CertificateOptions, DeprecatedVerbosityOptions};
/// Create and inspect IBM Secure Execution images.
///
/// Use pvimg to create an IBM Secure Execution image, which can be loaded using
/// zipl or QEMU. pvimg can also be used to inspect existing Secure Execution
/// images.
#[derive(Parser, Debug)]
#[command()]
pub struct CliOptions {
#[clap(flatten)]
pub verbose: DeprecatedVerbosityOptions,
/// Print version information and exit.
// Implemented for the help message only. Actual parsing happens in the
// version command.
#[arg(long)]
pub version: bool,
#[command(subcommand)]
pub cmd: SubCommands,
}
impl From<GenprotimgCliOptions> for CliOptions {
fn from(value: GenprotimgCliOptions) -> Self {
Self {
verbose: value.verbose,
version: false,
cmd: SubCommands::Create(value.args),
}
}
}
impl CliOptions {
pub fn new_version_cmd_opts() -> Self {
Self {
verbose: DeprecatedVerbosityOptions::default(),
version: true,
cmd: SubCommands::Version,
}
}
}
/// Validates the given command line options.
///
/// # Errors
///
/// This function will return an error if an argument is missing.
pub fn validate_cli(opts: &CliOptions) -> Result<(), clap::error::Error> {
match &opts.cmd {
SubCommands::Create(create_opts) => {
if let Some(dir) = create_opts
.experimental_args
.x_bootloader_directory
.as_ref()
{
warn!("Use bootloader directory: {}", dir.display());
}
Ok(())
}
_ => Ok(()),
}
}
/// CLI Argument collection for handling input components.
#[derive(Args, Debug)]
pub struct ComponentPaths {
/// Use the content of FILE as a raw binary Linux kernel.
///
/// The Linux kernel must be a raw binary s390x Linux kernel. The ELF format
/// is not supported.
#[arg(short='i', long = "kernel", value_name = "FILE", value_hint = ValueHint::FilePath, visible_alias = "image")]
pub kernel: PathBuf,
/// Use the content of FILE as the Linux initial RAM disk.
#[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath)]
pub ramdisk: Option<PathBuf>,
/// Use the content of FILE as the Linux kernel command line.
///
/// The Linux kernel command line must be shorter than the maximum kernel
/// command line size supported by the given Linux kernel.
#[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath)]
pub parmfile: Option<PathBuf>,
}
#[derive(Args, Debug)]
#[command(group(ArgGroup::new("header-flags").multiple(true).conflicts_with_all(["x_pcf", "x_scf"])))]
pub struct CreateBootImageLegacyFlags {
/// Enable Secure Execution guest dump support. This option requires the
/// '--comm-key' option.
#[arg(long, action = clap::ArgAction::SetTrue, requires="comm_key", group="header-flags")]
pub enable_dump: Option<bool>,
/// Disable Secure Execution guest dump support (default).
#[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_dump", group="header-flags")]
pub disable_dump: Option<bool>,
/// Add-secret requests must provide an extension secret that matches the
/// CCK-derived extension secret. This option requires the '--comm-key'
/// option.
#[arg(long, action = clap::ArgAction::SetTrue, requires="comm_key", group="header-flags")]
pub enable_cck_extension_secret: Option<bool>,
/// Add-secret requests don't have to provide the CCK-derived extension
/// secret (default).
#[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_cck_extension_secret", group="header-flags")]
pub disable_cck_extension_secret: Option<bool>,
/// Enable the support for the DEA, TDEA, AES, and ECC PCKMO key encryption
/// functions (default).
#[arg(long, action = clap::ArgAction::SetTrue, group="header-flags")]
pub enable_pckmo: Option<bool>,
/// Disable the support for the DEA, TDEA, AES, and ECC PCKMO key encryption
/// functions.
#[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_pckmo", group="header-flags")]
pub disable_pckmo: Option<bool>,
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum OutputFormat {
/// JSON format.
Json,
}
impl Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Json => "JSON",
}
)
}
}
#[derive(Args, Debug)]
pub struct SeImgInputArgs {
/// Use INPUT as the Secure Execution image.
#[arg(value_name = "INPUT", value_hint = ValueHint::FilePath,)]
pub path: PathBuf,
}
#[derive(Args, Debug)]
pub struct InfoArgs {
#[clap(flatten)]
pub input: SeImgInputArgs,
/// The output format
#[arg(long, value_enum)]
pub format: OutputFormat,
/// Use the key in FILE to decrypt the Secure Execution header.
#[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath,)]
pub key: Option<PathBuf>,
}
#[derive(Args, Debug)]
#[command(group(ArgGroup::new("test-args").multiple(true).required(true)))]
pub struct TestArgs {
#[clap(flatten)]
pub input: SeImgInputArgs,
/// Use FILE to check for a host key document.
///
/// Verifies that the image contains the host key hash of one of the
/// specified host keys. The check fails if none of the host keys match the
/// hash in the image. This parameter can be specified multiple times.
/// Mutually exclusive with '--key-hashes'.
#[arg(
short = 'k',
long = "host-key-document",
value_name = "FILE",
value_hint = ValueHint::FilePath,
use_value_delimiter = true,
value_delimiter = ',',
group = "test-args",
)]
pub host_key_documents: Vec<PathBuf>,
/// Use FILE to check for the host key hashes provided by the ultravisor. If
/// no FILE is specified, FILE defaults to '/sys/firmware/uv/keys/all'.
///
/// The default file is only available if the local system supports the
/// Query Ultravisor Keys UVC. Verifies that the image contains the host key
/// hash of one of the specified hashes in FILE. The check fails if none of
/// the host keys match a hash in the response. Mutually exclusive with
/// '--host-key-document'.
#[arg(
long = "key-hashes",
value_name = "FILE",
value_hint = ValueHint::FilePath,
num_args = 0..=1,
require_equals = true,
default_missing_value = "/sys/firmware/uv/keys/all",
conflicts_with="host_key_documents",
group = "test-args",
)]
pub key_hashes: Option<PathBuf>,
}
/// Create an IBM Secure Execution image.
///
/// Create a new IBM Secure Execution image. Only create these images in a
/// trusted environment, such as your workstation. The 'genprotimg' command
/// creates randomly generated keys to protect the image. The generated image
/// can then be booted on an IBM Secure Execution system as a KVM guest.
///
/// Note: The 'genprotimg' command is a symbolic link to the 'pvimg create'
/// command.
#[derive(Parser, Debug)]
pub struct GenprotimgCliOptions {
#[clap(flatten)]
pub args: Box<CreateBootImageArgs>,
#[clap(flatten)]
pub verbose: DeprecatedVerbosityOptions,
/// Print version information and exit.
// Implemented for the help message only. Actual parsing happens in the
// version command.
#[arg(long, action = clap::ArgAction::SetTrue )]
pub version: (),
}
impl GenprotimgCliOptions {
pub fn command() -> Command {
let cmd = <Self as CommandFactory>::command();
// Make sure that the correct binary is shown in the clap error
// messages.
cmd.bin_name("genprotimg")
}
pub fn own_parse() -> CliOptions {
let args = env::args_os();
let args_len = args.len();
let version_count = args.filter(|value| value == "--version").count();
if version_count > 1 || version_count == 1 && (args_len != version_count + 1) {
Self::command()
.error(
clap::error::ErrorKind::UnknownArgument,
"unexpected argument",
)
.exit()
}
if version_count == 1 {
CliOptions::new_version_cmd_opts()
} else {
let genprotimg_opts = Self::parse();
genprotimg_opts.into()
}
}
}
#[derive(Parser, Debug)]
pub struct CreateBootImageArgs {
#[clap(flatten)]
pub component_paths: ComponentPaths,
/// Write the generated Secure Execution boot image to FILE.
#[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath,)]
pub output: PathBuf,
#[clap(flatten)]
pub certificate_args: CertificateOptions,
/// Disable all input component checks.
///
/// For example, for the Linux kernel, it tests if the given kernel looks
/// like a raw binary s390x kernel.
#[arg(long)]
pub no_component_check: bool,
/// Overwrite an existing Secure Execution boot image.
#[arg(long)]
pub overwrite: bool,
/// Use the content of FILE as the customer-communication key (CCK).
///
/// The file must contain exactly 32 bytes of data.
#[arg(long, value_name = "FILE")]
pub comm_key: Option<PathBuf>,
#[clap(flatten)]
pub legacy_flags: CreateBootImageLegacyFlags,
#[clap(flatten)]
pub experimental_args: CreateBootImageExperimentalArgs,
}
/// Experimental options
#[derive(Args, Debug)]
pub struct CreateBootImageExperimentalArgs {
/// Manually set the directory used to load the Secure Execution bootloaders
/// (stage3a and stage3b) (experimental option).
// Hidden in user documentation.
#[arg(long, value_name = "DIR", hide(true))]
pub x_bootloader_directory: Option<PathBuf>,
/// Manually set the image components encryption key (experimental option).
// Hidden in user documentation.
#[arg(long, value_name = "FILE", hide(true))]
pub x_comp_key: Option<PathBuf>,
/// Manually set the Secure Execution header protection key (experimental option).
// Hidden in user documentation.
#[arg(long, value_name = "FILE", hide(true))]
pub x_header_key: Option<PathBuf>,
/// Manually set the PSW address used for the Secure Execution header (experimental option).
// Hidden in user documentation.
#[arg(long, value_name = "ADDRESS", hide(true))]
pub x_psw: Option<String>,
/// Manually set the plaintext control flags (experimental option).
// No validity checks made. Hidden in user documentation.
#[arg(long, value_name = "PCF", hide(true))]
pub x_pcf: Option<String>,
/// Manually set the secret control flags (experimental option).
// No validity checks made. Hidden in user documentation.
#[arg(long, value_name = "SCF", hide(true))]
pub x_scf: Option<String>,
}
#[derive(Debug, clap::Subcommand)]
pub enum SubCommands {
/// Create an IBM Secure Execution image.
///
/// Create a new IBM Secure Execution image. Only create these images in a
/// trusted environment, such as your workstation. The 'pvimg create'
/// command creates randomly generated keys to protect the image. The
/// generated image can then be booted on an IBM Secure Execution system as
/// a KVM guest.
Create(Box<CreateBootImageArgs>),
/// Print information about the IBM Secure Execution image.
///
/// Note that the API and output format is experimental and subject to
/// change.
Info(InfoArgs),
/// Test different aspects of an existing IBM Secure Execution image.
Test(Box<TestArgs>),
/// Print version information and exit.
#[command(aliases(["--version"]), hide(true))]
Version,
}
#[allow(clippy::shadow_unrelated)]
#[cfg(test)]
mod test {
use std::collections::BTreeMap;
use super::*;
#[derive(Hash, Eq, PartialEq, Debug, Clone)]
struct CliOption {
name: String,
args: Vec<String>,
}
impl CliOption {
fn new<S: AsRef<str>, T: AsRef<str>, P: AsRef<[S]>>(name: T, args: P) -> Self {
let name = name.as_ref().to_owned();
let args = args
.as_ref()
.iter()
.map(|v| v.as_ref().to_owned())
.collect();
Self { name, args }
}
}
impl From<CliOption> for Vec<String> {
fn from(val: CliOption) -> Self {
let CliOption { args, .. } = val;
args
}
}
fn flat_map_collect(map: BTreeMap<String, CliOption>) -> Vec<String> {
map.into_values().flat_map(|v| v.args).collect()
}
fn insert(
mut map: BTreeMap<String, CliOption>,
values: Vec<CliOption>,
) -> BTreeMap<String, CliOption> {
for value in values {
map.insert(value.name.to_owned(), value);
}
map
}
fn remove<S: AsRef<str>>(
mut map: BTreeMap<String, CliOption>,
key: S,
) -> BTreeMap<String, CliOption> {
map.remove(key.as_ref());
map
}
#[test]
#[rustfmt::skip]
fn genprotimg_and_pvimg_create_args() {
// Minimal valid create arguments using no-verify
let mut mvcanv = BTreeMap::new();
mvcanv = insert(mvcanv, vec![CliOption::new("image", ["--image", "/dev/null"])]);
mvcanv = insert(mvcanv, vec![CliOption::new("hkd", ["--host-key-document", "/dev/null"])]);
mvcanv = insert(mvcanv, vec![CliOption::new("output", ["--output", "/dev/null"])]);
mvcanv = insert(mvcanv, vec![CliOption::new("no-verify", ["--no-verify"])]);
// Minimal valid create arguments using --cert
let mut mvca = mvcanv.clone();
mvca.remove("no-verify");
mvca = insert(mvca, vec![CliOption::new("cert", ["--cert", "/dev/null"])]);
let valid_create_args = [
flat_map_collect(mvcanv.clone()),
flat_map_collect(insert(remove(mvcanv.clone(), "image"), vec![CliOption::new("kernel", ["--kernel", "/dev/kernel"])])),
flat_map_collect(insert(mvcanv.clone(), vec![CliOption::new("root-ca", ["--root-ca", "/dev/null"])])),
flat_map_collect(mvca.clone()),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("quiet", ["-q"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("verbose", ["-vvv"])])),
// Verify the old verbosity is still working.
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("verbose", ["-VVV"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("offline", ["--offline"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("ramdisk", ["--ramdisk", "/dev/null"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("parmfile", ["--parmfile", "/dev/null"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-dump", ["--enable-dump"]),
CliOption::new("comm-key", ["--comm-key", "/dev/null"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-dump", ["--enable-dump"]),
CliOption::new("comm-key", ["--comm-key", "/dev/null"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-pcf", ["--x-pcf", "0x0"]),
CliOption::new("x-scf", ["--x-scf", "0x0"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-psw", ["--x-psw", "0x0"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("no-component-check", ["--no-component-check"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-pckmo", ["--enable-pckmo"])])),
];
let invalid_create_args = [
flat_map_collect(remove(mvcanv.clone(), "no-verify")),
flat_map_collect(remove(mvcanv.clone(), "image")),
flat_map_collect(remove(mvcanv.clone(), "hkd")),
flat_map_collect(remove(mvcanv, "output")),
// missing `--comm-key`
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-dump", ["--enable-dump"])])),
// -v and -q cannot be combined
flat_map_collect(insert(mvca.clone(), vec![
CliOption::new("verbose", ["-v"]),
CliOption::new("quiet", ["-q"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("image2", ["--image", "/dev/null"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("output2", ["--output", "/dev/null"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("ramdisk", ["--ramdisk", "/dev/null"]),
CliOption::new("ramdisk2", ["--ramdisk", "/dev/null"]) ])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("parmfile", ["--parmfile", "/dev/null"]),
CliOption::new("parmfile2", ["--parmfile", "/dev/null"]) ])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("x-pcf", ["--x-pcf", "0x0"]),
CliOption::new("x-pcf2", ["--x-pcf", "0x0"])])),
flat_map_collect(insert(mvca.clone(), vec![CliOption::new("enable-pckmo", ["--enable-pckmo"]),
CliOption::new("disable-pckmo", ["--disable-pckmo"])])),
];
let mut genprotimg_valid_args = vec![
// See workaround `parse_version` in `pvimg/main.rs`.
// vec!["genprotimg", "--version"],
];
let mut pvimg_valid_args = vec![
vec!["pvimg", "--version"],
vec!["pvimg", "version"],
];
// Test for invalid combinations
let mut genprotimg_invalid_args = vec![
vec!["genprotimg"],
];
let mut pvimg_invalid_args = vec![
vec!["pvimg"],
];
// Test that `genprotimg` and `pvimg create` behave equally.
for create_args in &valid_create_args {
genprotimg_valid_args.push([["genprotimg"].to_vec(), Vec::from_iter(create_args.iter().map(String::as_str))].concat());
pvimg_valid_args.push([["pvimg", "create"].to_vec(), Vec::from_iter(create_args.iter().map(String::as_str))].concat());
}
for invalid_create_args in &invalid_create_args {
genprotimg_invalid_args.push([["genprotimg"].to_vec(), Vec::from_iter(invalid_create_args.iter().map(String::as_str))].concat());
pvimg_invalid_args.push([["pvimg", "create"].to_vec(), Vec::from_iter(invalid_create_args.iter().map(String::as_str))].concat());
}
for arg in pvimg_valid_args {
let res = CliOptions::try_parse_from(&arg);
#[allow(clippy::use_debug, clippy::print_stdout)]
if let Err(e) = &res {
println!("arg: {arg:?}");
println!("{e}");
}
assert!(res.is_ok());
}
for arg in pvimg_invalid_args {
let res = CliOptions::try_parse_from(&arg);
assert!(res.is_err());
}
for arg in genprotimg_valid_args {
let res = GenprotimgCliOptions::try_parse_from(&arg);
#[allow(clippy::use_debug, clippy::print_stdout)]
if let Err(e) = &res {
println!("arg: {arg:?}");
println!("{e}");
}
assert!(res.is_ok());
}
for arg in genprotimg_invalid_args {
let res = GenprotimgCliOptions::try_parse_from(&arg);
assert!(res.is_err());
}
}
#[test]
fn pvimg_test_cli() {
let args = BTreeMap::new();
let valid_test_args = [
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("host-key-hashes", ["--key-hashes"]),
CliOption::new("image", ["/dev/null"]),
],
)),
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("host-key-hashes2", ["--key-hashes=/dev/null"]),
CliOption::new("image", ["/dev/null"]),
],
)),
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("host-key-hashes2", ["--key-hashes=/dev/null"]),
CliOption::new("image", ["/dev/null"]),
// global works
CliOption::new("quiet", ["-q"]),
],
)),
// separation between keyword and positional args works
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("host-key-hashes2", ["--key-hashes=/dev/null"]),
CliOption::new("image", ["--", "/dev/null"]),
],
)),
// Verify that the old verbosity is still working.
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("host-key-hashes2", ["--key-hashes=/dev/null"]),
CliOption::new("image", ["/dev/null"]),
CliOption::new("verbose", ["-VVV"]),
],
)),
];
let invalid_test_args = [
flat_map_collect(insert(
args.clone(),
vec![CliOption::new("image", ["/dev/null"])],
)),
// the argument '--key-hashes[=<FILE>]' cannot be used with '--host-key-document
// <FILE>'
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("host-key-hashes2", ["--key-hashes=/dev/null"]),
CliOption::new("host-key-document", ["--host-key-document", "/dev/null"]),
CliOption::new("image", ["/dev/null"]),
],
)),
flat_map_collect(insert(
args,
vec![
CliOption::new("host-key-hashes2", ["--key-hashes", "/sys/null"]),
CliOption::new("image", ["--", "/dev/null"]),
],
)),
];
let mut pvimg_valid_args = vec![];
// Test for invalid combinations
// Input is missing
let mut pvimg_invalid_args = vec![vec!["pvimg", "test"]];
for create_args in &valid_test_args {
pvimg_valid_args.push(
[
["pvimg", "test"].to_vec(),
Vec::from_iter(create_args.iter().map(String::as_str)),
]
.concat(),
);
}
for invalid_test_arg in &invalid_test_args {
pvimg_invalid_args.push(
[
["pvimg", "test"].to_vec(),
Vec::from_iter(invalid_test_arg.iter().map(String::as_str)),
]
.concat(),
);
}
for arg in pvimg_valid_args {
let res = CliOptions::try_parse_from(&arg);
#[allow(clippy::use_debug, clippy::print_stdout)]
if let Err(e) = &res {
println!("arg: {arg:?}");
println!("{e}");
}
assert!(res.is_ok());
}
for arg in pvimg_invalid_args {
let res = CliOptions::try_parse_from(&arg);
assert!(res.is_err());
}
}
#[test]
fn pvimg_info_cli() {
let args = BTreeMap::new();
let valid_test_args = [
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("format", ["--format", "json"]),
CliOption::new("image", ["/dev/null"]),
],
)),
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("format", ["--format=json"]),
CliOption::new("image", ["/dev/null"]),
],
)),
// separation between keyword and positional args works
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("format", ["--format=json"]),
CliOption::new("image", ["--", "/dev/null"]),
],
)),
// Verify that the old verbosity is still working.
flat_map_collect(insert(
args.clone(),
vec![
CliOption::new("format", ["--format=json"]),
CliOption::new("image", ["/dev/null"]),
CliOption::new("verbose", ["-VVV"]),
],
)),
];
let invalid_test_args = [
// the argument '--key-hashes[=<FILE>]' cannot be used with '--host-key-document
// <FILE>'
flat_map_collect(insert(
args.clone(),
vec![CliOption::new("image", ["/dev/null"])],
)),
// No default defined for --format
flat_map_collect(insert(
args,
vec![
CliOption::new("format", ["--format"]),
CliOption::new("image", ["--", "/dev/null"]),
],
)),
];
let mut pvimg_valid_args = vec![];
// Test for invalid combinations
// Input is missing
let mut pvimg_invalid_args = vec![vec!["pvimg", "test"]];
for create_args in &valid_test_args {
pvimg_valid_args.push(
[
["pvimg", "info"].to_vec(),
Vec::from_iter(create_args.iter().map(String::as_str)),
]
.concat(),
);
}
for invalid_test_arg in &invalid_test_args {
pvimg_invalid_args.push(
[
["pvimg", "info"].to_vec(),
Vec::from_iter(invalid_test_arg.iter().map(String::as_str)),
]
.concat(),
);
}
for arg in pvimg_valid_args {
let res = CliOptions::try_parse_from(&arg);
#[allow(clippy::use_debug, clippy::print_stdout)]
if let Err(e) = &res {
println!("arg: {arg:?}");
println!("{e}");
}
assert!(res.is_ok());
}
for arg in pvimg_invalid_args {
let res = CliOptions::try_parse_from(&arg);
assert!(res.is_err());
}
}
#[test]
fn verify_cli() {
use clap::CommandFactory;
CliOptions::command().debug_assert();
}
}

16
rust/pvimg/src/cmd.rs Normal file
View File

@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
mod common;
mod create;
mod info;
mod test;
mod version;
pub const CMD_FN: &[&str] = &["+create", "+test", "+info"];
pub use create::create;
pub use info::info;
pub use test::test;
pub use version::version;

View File

@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::path::{Path, PathBuf};
use anyhow::Result;
use log::info;
use pv::{misc::read_file, request::Confidential};
use crate::cli::CreateBootImageExperimentalArgs;
#[macro_export]
/// Makes it easier to
macro_rules! log_println {
($($arg:tt)+) => { warn!($($arg)+) };
}
pub struct UserProvidedKeys {
pub(crate) cck: Option<(PathBuf, Confidential<Vec<u8>>)>,
pub(crate) components_key: Option<(PathBuf, Confidential<Vec<u8>>)>,
pub(crate) aead_key: Option<(PathBuf, Confidential<Vec<u8>>)>,
}
/// Reads all user provided keys.
pub fn read_user_provided_keys(
cck_path: Option<&Path>,
experimental_args: &CreateBootImageExperimentalArgs,
) -> Result<UserProvidedKeys> {
let components_key = {
match &experimental_args.x_comp_key {
Some(key_path) => {
info!(
"Use file '{}' as the image components protection key",
key_path.display()
);
Some((
key_path.to_owned(),
Confidential::new(read_file(key_path, "image components key")?),
))
}
None => None,
}
};
let aead_key = {
match &experimental_args.x_header_key {
Some(key_path) => {
info!(
"Use file '{}' as the Secure Execution header protection",
key_path.display()
);
Some((
key_path.to_owned(),
Confidential::new(read_file(
key_path,
"Secure Execution header protection key",
)?),
))
}
None => None,
}
};
let cck = {
match cck_path {
Some(key_path) => {
info!(
"Use file '{}' as the customer communication key (CCK)",
key_path.display()
);
Some((
key_path.to_owned(),
(Confidential::new(read_file(key_path, "customer communication key (CCK)")?)),
))
}
None => None,
}
};
Ok(UserProvidedKeys {
cck,
components_key,
aead_key,
})
}

View File

@@ -0,0 +1,196 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::{fs::OpenOptions, io::BufReader};
use anyhow::{Context, Result};
use log::{debug, warn};
use pv::misc::{open_file, try_parse_u64};
use pvimg::{
error::OwnExitCode,
secured_comp::ComponentTrait,
uvdata::{
ControlFlagTrait, ControlFlagsTrait, FlagData, PcfV1, PlaintextControlFlagsV1, ScfV1,
SeHdrDataV1, SecretControlFlagsV1,
},
};
use utils::{AtomicFile, AtomicFileOperation};
use crate::{
cli::{ComponentPaths, CreateBootImageArgs},
cmd::common::read_user_provided_keys,
se_img::{SeHdrArgs, SeImgBuilder},
se_img_comps::{
check_components, cmdline::Cmdline, kernel::S390Kernel, ramdisk::Ramdisk, Component,
},
};
/// The returned vector is sorted by the occurrence in the memory layout:
/// First the kernel, then the ramdisk and then the kernel cmdline.
///
/// Keep this ordering in sync with the ordering of [`ComponentKind`]!
fn components(component_args: &ComponentPaths) -> Result<Vec<Component>> {
// IMPORTANT: Don't change the order of the components: kernel, ramdisk, and
// then parmline! This is important since ALD, PLD and TLD is sorted by the
// component address.
let mut components: Vec<Component> =
vec![S390Kernel::new(Box::new(BufReader::new(open_file(&component_args.kernel)?))).into()];
if let Some(path) = &component_args.ramdisk {
components.push(Ramdisk::new(Box::new(BufReader::new(open_file(path)?))).into());
}
if let Some(path) = &component_args.parmfile {
components.push(Cmdline::new(Box::new(BufReader::new(open_file(path)?))).into());
}
Ok(components)
}
fn parse_flags(
args: &CreateBootImageArgs,
) -> Result<(PlaintextControlFlagsV1, SecretControlFlagsV1)> {
let lf = &args.legacy_flags;
let plaintext_flags: Vec<FlagData<PcfV1>> = [
lf.disable_dump
.filter(|x| *x)
.and(Some(PcfV1::all_disabled([PcfV1::AllowDumping]))),
lf.enable_dump
.filter(|x| *x)
.and(Some(PcfV1::all_disabled([PcfV1::AllowDumping]))),
lf.disable_pckmo
.filter(|x| *x)
.and(Some(PcfV1::all_disabled([
PcfV1::PckmoAes,
PcfV1::PckmoDeaTdea,
PcfV1::PckmoEcc,
]))),
lf.enable_pckmo.filter(|x| *x).and(Some(PcfV1::all_enabled([
PcfV1::PckmoAes,
PcfV1::PckmoDeaTdea,
PcfV1::PckmoEcc,
]))),
]
.into_iter()
.flatten()
.flatten()
.collect();
// This is ensured by Clap's `conflicts_with`.
assert!(PlaintextControlFlagsV1::no_duplicates(&plaintext_flags));
let secret_flags: Vec<FlagData<ScfV1>> = [
lf.disable_cck_extension_secret
.filter(|x| *x)
.and(Some(ScfV1::all_disabled([
ScfV1::CCKExtensionSecretEnforcment,
]))),
lf.enable_cck_extension_secret
.filter(|x| *x)
.and(Some(ScfV1::all_enabled([
ScfV1::CCKExtensionSecretEnforcment,
]))),
]
.into_iter()
.flatten()
.flatten()
.collect();
// This is ensured by Clap's `conflicts_with`.
assert!(SecretControlFlagsV1::no_duplicates(&secret_flags));
let mut pcf: PlaintextControlFlagsV1 = match &args.experimental_args.x_pcf {
Some(v) => try_parse_u64(v, "x-pcf")?.into(),
None => PlaintextControlFlagsV1::default(),
};
pcf.parse_flags(&plaintext_flags);
debug!("Using plaintext flags: {pcf}");
let mut scf: SecretControlFlagsV1 = match &args.experimental_args.x_scf {
Some(v) => try_parse_u64(v, "x-scf")?.into(),
None => SecretControlFlagsV1::default(),
};
scf.parse_flags(&secret_flags);
debug!("Using secret flags: {scf}");
Ok((pcf, scf))
}
/// Create a Secure Execution boot image
pub fn create(opt: &CreateBootImageArgs) -> Result<OwnExitCode> {
// Verify host key documents first, because if they are not valid there is
// no reason to continue.
let verified_host_keys = opt
.certificate_args
.get_verified_hkds("Secure Execution image")?;
let user_provided_keys =
read_user_provided_keys(opt.comm_key.as_deref(), &opt.experimental_args)?;
let (plaintext_flags, secret_flags) = parse_flags(opt)?;
let mut components = components(&opt.component_paths)?;
if opt.no_component_check {
warn!("The component check is turned off!");
} else {
check_components(&mut components)?;
}
// FIXME get rid of the legacy mode. But that's only possible as soon as all
// available tools are updated.
let expected_se_hdr_size = SeHdrDataV1::expected_size(verified_host_keys.len())?;
let mut writer = AtomicFile::with_extension(&opt.output, "part", &mut OpenOptions::new())?;
let mut seimg_ctx = SeImgBuilder::new_v1(
&mut writer,
plaintext_flags.is_unset(PcfV1::NoComponentEncryption),
Some(expected_se_hdr_size),
opt.experimental_args.x_bootloader_directory.as_ref(),
)?;
// Enable expert mode
seimg_ctx.i_know_what_i_am_doing();
if let Some((path, key)) = user_provided_keys.components_key {
seimg_ctx.set_components_key(key).with_context(|| {
format!(
"Failed to use '{}' as the image components key",
path.display()
)
})?;
}
let psw_addr: Option<u64> = match &opt.experimental_args.x_psw {
Some(v) => try_parse_u64(v, "x-psw")?.into(),
None => None,
};
for mut component in components.into_iter() {
seimg_ctx
.prepare_and_append_as_secure_component(&mut component, None)
.with_context(|| format!("Failed to prepare {} component", component.kind()))?;
}
let img_comps = seimg_ctx.finish(SeHdrArgs {
keys: verified_host_keys.as_slice(),
pcf: &plaintext_flags,
scf: &secret_flags,
cck: &user_provided_keys.cck,
hdr_aead_key: &user_provided_keys.aead_key,
psw_addr: &psw_addr,
})?;
debug!("");
debug!("----------------------------------------------------------------");
debug!("| {:^60} |", "Secure Execution image layout");
debug!("|--------------------------------------------------------------|");
debug!("| {:<23} | {:<34} |", "Component type", "Component address");
debug!("|-------------------------|------------------------------------|");
img_comps
.iter()
.for_each(|img_comp| debug!("{img_comp:<33}"));
debug!("----------------------------------------------------------------");
// Rename the file `$OUTPUT.part` to `$OUTPUT` for achieving atomic file
// creation.
let op = match opt.overwrite {
true => AtomicFileOperation::Replace,
false => AtomicFileOperation::NoReplace,
};
writer.finish(op)?;
warn!("Successfully generated the Secure Execution image.");
Ok(OwnExitCode::Success)
}

View File

@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::io::Write;
use anyhow::Result;
use log::info;
use pv::{
misc::{open_file, read_file},
request::SymKey,
};
use pvimg::{
error::OwnExitCode,
uvdata::{KeyExchangeTrait, SeHdr, UvDataTrait},
};
use crate::cli::InfoArgs;
pub fn info(opt: &InfoArgs) -> Result<OwnExitCode> {
info!(
"Reading Secure Execution header {}",
opt.input.path.display()
);
let mut input = open_file(&opt.input.path)?;
let mut output = std::io::stdout();
SeHdr::seek_sehdr(&mut input, None)?;
let hdr = SeHdr::try_from_io(input)?;
if let Some(key_path) = &opt.key {
let key =
SymKey::try_from_data(hdr.key_type(), read_file(key_path, "Reading key")?.into())?;
serde_json::to_writer_pretty(&mut output, &hdr.decrypt(&key)?)?;
} else {
serde_json::to_writer_pretty(&mut output, &hdr)?;
}
writeln!(output)?;
Ok(OwnExitCode::Success)
}

122
rust/pvimg/src/cmd/test.rs Normal file
View File

@@ -0,0 +1,122 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::path::{Path, PathBuf};
use anyhow::Result;
use log::{info, warn};
use pv::{
misc::{open_file, read_certs, read_file},
FileAccessErrorType, PvCoreError,
};
use pvimg::{
error::{Error, OwnExitCode, PvError},
uvdata::{KeyExchangeTrait, SeHdr, UvKeyHashesV1},
};
use utils::HexSlice;
use crate::{cli::TestArgs, log_println};
/// Returns `Ok(true)` if at least one of the hashes is included.
fn hdr_test_target_hashes(hdr: &SeHdr, key_hashes: &Path) -> Result<bool> {
let file = open_file(key_hashes).map_err(|err| match err {
PvCoreError::FileAccess {
ref ty,
ref path,
ref source,
} if matches!(ty, FileAccessErrorType::Open)
&& source.kind() == std::io::ErrorKind::NotFound
&& *path == PathBuf::from(UvKeyHashesV1::SYS_UV_KEYS_ALL) =>
{
Error::UnavailableQueryUvKeyHashesSupport { source: err }
}
err => Error::PvCore(err),
})?;
let hashes = UvKeyHashesV1::read_from_io(file)?;
let mut contains = hdr.contains_hash(&hashes.pchkh);
if contains {
log_println!(
" ✓ Host key hash {:#} is included",
HexSlice::from(&hashes.pchkh)
);
}
if hdr.contains_hash(&hashes.pbhkh) {
log_println!(
" ✓ Backup host key hash {:#} is included",
HexSlice::from(&hashes.pbhkh)
);
contains = true;
};
for hash in hashes.res {
if hdr.contains_hash(&hash) {
log_println!(" ✓ Key hash {:#} is included", HexSlice::from(&hash));
contains = true;
}
}
if !contains {
warn!(" ✘ None of the key hashes is included");
}
Ok(contains)
}
/// Returns `Ok(true)` if at least one of the given public key of the host key
/// documents was used for the image creation or if no host key document was
/// specified.
fn hdr_test_hkd<P>(hdr: &SeHdr, host_key_documents: &[P]) -> Result<bool>
where
P: AsRef<Path>,
{
if host_key_documents.is_empty() {
return Ok(true);
}
let mut result = false;
for path in host_key_documents {
let hkd_path = path.as_ref();
let hkd_data = read_file(hkd_path, "host key document")?;
let certs = read_certs(&hkd_data)?;
if certs.is_empty() {
return Err(PvError::NoHkdInFile(hkd_path.display().to_string()).into());
}
if certs.len() != 1 {
warn!("The host key document in '{}' contains more than one certificate! Only the first certificate will be used.",
hkd_path.display());
}
// Panic: len is == 1 -> unwrap will succeed/not panic
let cert = certs.first().unwrap();
if hdr.contains(cert.public_key()?)? {
result = true;
log_println!(" ✓ Host key document '{}' is included", hkd_path.display());
} else {
log_println!(
" ✘ Host key document '{}' is not included",
hkd_path.display()
);
}
}
Ok(result)
}
pub fn test(opt: &TestArgs) -> Result<OwnExitCode> {
info!("Testing a Secure Execution image");
let mut input = open_file(&opt.input.path)?;
SeHdr::seek_sehdr(&mut input, None)?;
let hdr = SeHdr::try_from_io(input)?;
let mut success = hdr_test_hkd(&hdr, &opt.host_key_documents)?;
if let Some(path) = &opt.key_hashes {
success = hdr_test_target_hashes(&hdr, path)? && success;
}
Ok(if success {
OwnExitCode::Success
} else {
OwnExitCode::GenericError
})
}

View File

@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use anyhow::Result;
use log::LevelFilter;
use pvimg::error::OwnExitCode;
use utils::print_version;
use crate::cmd;
const FEATURES: &[&[&str]] = &[cmd::CMD_FN];
/// Print the version
pub fn version(filter: LevelFilter) -> Result<OwnExitCode> {
print_version!("2024", filter; FEATURES.concat());
Ok(OwnExitCode::Success)
}

71
rust/pvimg/src/main.rs Normal file
View File

@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
//! # pvimg
//!
//! `pvimg` is a command line utility to create and inspect IBM Secure
//! Execution boot images.
//!
//! Use `pvimg` to create a IBM Secure Execution boot image file, which can
//! be loaded using `zipl` or `QEMU`. The tool can also be used to inspect
//! existing Secure Execution boot images.
mod cli;
mod cmd;
mod se_img;
mod se_img_comps;
use std::{env, process::ExitCode};
use clap::{Command, CommandFactory, Parser};
use cli::{validate_cli, CliOptions, SubCommands};
use log::trace;
use pvimg::error::OwnExitCode;
use utils::{print_cli_error, print_error, PvLogger};
use crate::cli::GenprotimgCliOptions;
static LOGGER: PvLogger = PvLogger;
fn main() -> ExitCode {
let exe = env::args_os().next().unwrap();
let (opts, cmd): (CliOptions, Command) = match exe.to_str() {
// Test if the symlink executable 'genprotimg' was used. If so use the
// `pvimg create` command directly.
Some(val) if val.ends_with("genprotimg") => (
GenprotimgCliOptions::own_parse(),
GenprotimgCliOptions::command(),
),
_ => (CliOptions::parse(), CliOptions::command()),
};
let verbosity = opts.verbose.to_level_filter();
if let Err(e) = LOGGER.start(verbosity) {
unreachable!("Logger error: {e:?}");
}
match validate_cli(&opts) {
Ok(opts) => opts,
Err(e) => {
let _ = print_cli_error(e, cmd);
return OwnExitCode::UsageError.into();
}
};
// NOTE trace verbosity is disabled in release builds
trace!("Trace verbosity, may leak secrets to command-line");
trace!("Options {opts:?}");
let res = match &opts.cmd {
SubCommands::Create(opt) => cmd::create(opt),
SubCommands::Info(opt) => cmd::info(opt),
SubCommands::Test(opt) => cmd::test(opt),
SubCommands::Version => cmd::version(verbosity),
};
match res {
Ok(own_exit_code) => own_exit_code.into(),
Err(e) => print_error(&e, verbosity),
}
}

488
rust/pvimg/src/se_img.rs Normal file
View File

@@ -0,0 +1,488 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::{
fmt::Display,
io::{Cursor, Seek, SeekFrom, Write},
path::PathBuf,
rc::Rc,
};
use anyhow::{anyhow, Context, Result};
use deku::DekuContainerRead;
use log::debug;
use openssl::pkey::{PKey, Public};
use pv::{misc::read_file, request::Confidential};
use pvimg::{
error::Error,
misc::{round_up, serialize_to_bytes, ShortPsw, PSW, PSW_MASK_BA, PSW_MASK_EA},
secured_comp::{ComponentTrait, Interval, Layout, SecuredComponent, SecuredComponentBuilder},
uvdata::{
BuilderTrait, PlaintextControlFlagsV1, SeHdrBuilder, SeHdrVersion, SecretControlFlagsV1,
},
};
use crate::se_img_comps::{
create_ipib, ipib::Ipib, kernel::S390Kernel, render_stage3a, render_stage3b, sehdr::SeHdrComp,
shortpsw::ShortPSWComp, stage3a_path, stage3b_path, CompTweakV1, Component, ComponentKind,
STAGE3A_ENTRY, STAGE3A_INIT_ENTRY, STAGE3A_LOAD_ADDRESS,
};
pub struct SeHdrArgs<'a> {
pub keys: &'a [PKey<Public>],
pub pcf: &'a PlaintextControlFlagsV1,
pub scf: &'a SecretControlFlagsV1,
pub cck: &'a Option<(PathBuf, Confidential<Vec<u8>>)>,
pub hdr_aead_key: &'a Option<(PathBuf, Confidential<Vec<u8>>)>,
pub psw_addr: &'a Option<u64>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ImgComponent {
kind: ComponentKind,
pub(crate) src: Rc<Interval>,
pub(crate) secure_mode: Option<SecuredComponent>,
}
impl ImgComponent {
pub fn kind(&self) -> ComponentKind {
self.kind.clone()
}
}
impl Ord for ImgComponent {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.src.cmp(&other.src)
}
}
impl PartialOrd for ImgComponent {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Display for ImgComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "| {:23} | ", self.kind.to_string())?;
self.src.to_string().fmt(f)?;
write!(f, " |")
}
}
pub struct SeImgBuilder<W> {
/// Expert mode (components encryption key and Secure Execution header
/// protection key can be set). By default disabled.
expert_mode: bool,
writer: W,
layout: Layout,
comps: Vec<Rc<ImgComponent>>,
builder: SecuredComponentBuilder,
stage3a: Vec<u8>,
stage3b: Vec<u8>,
/// The legacy Secure Execution header address (directly after stage3a)
legacy_se_hdr_addr: Option<u64>,
finalized: bool,
}
impl<W: Write + Seek> SeImgBuilder<W> {
const COMPONENT_ALIGNMENT_V1: u64 = SecuredComponentBuilder::COMPONENT_ALIGNMENT_V1;
const DEFAULT_INITIAL_PSW_MASK: u64 = PSW_MASK_BA | PSW_MASK_EA;
/// Create a Secure Execution boot image builder
#[allow(clippy::similar_names)]
pub(crate) fn new_v1(
mut writer: W,
encryption: bool,
legacy_expected_se_hdr_size: Option<usize>,
bootloader_dir: Option<&PathBuf>,
) -> Result<Self> {
let stage3a = read_file(stage3a_path(bootloader_dir), "stage3a")?;
let stage3b = read_file(stage3b_path(bootloader_dir), "stage3b")?;
let mut legacy_se_hdr_addr = None;
// Reserve memory space for the stage3a loader that will be written
// later.
let mut next_comp_addr: u64 = round_up(
STAGE3A_LOAD_ADDRESS
.checked_add(stage3a.len().try_into()?)
.ok_or(Error::UnexpectedOverflow)?,
Self::COMPONENT_ALIGNMENT_V1,
)?;
// Reserve memory space for the Secure Execution header in case of
// legacy mode.
if let Some(expected_se_hdr_size) = legacy_expected_se_hdr_size {
// Place the Secure Execution header next to the stage3a and use as
// the minimum address 0x14000. 0x14000 is used as the starting
// point for searching the Secure Execution header in the
// `pvextract-hdr` utility and we can therefore not use e.g. 0x13000
// even if it would be possible in regard to the memory layout.
const PV_EXTRACT_SE_HDR_SEARCH_ADDR: u64 = 0x14000;
let se_hdr_addr = std::cmp::max(next_comp_addr, PV_EXTRACT_SE_HDR_SEARCH_ADDR);
next_comp_addr = round_up(
se_hdr_addr
.checked_add(expected_se_hdr_size.try_into()?)
.ok_or(Error::UnexpectedOverflow)?,
Self::COMPONENT_ALIGNMENT_V1,
)?;
legacy_se_hdr_addr = Some(se_hdr_addr);
}
if next_comp_addr % Self::COMPONENT_ALIGNMENT_V1 != 0 {
return Err(Error::UnalignedAddress {
addr: next_comp_addr,
alignment: Self::COMPONENT_ALIGNMENT_V1,
}
.into());
}
// Secure Execution expects, that the component addresses are aligned to
// 4096.
let layout = Layout::new(next_comp_addr, Self::COMPONENT_ALIGNMENT_V1)?;
let builder = SecuredComponentBuilder::new_v1(encryption)?;
// The layout of the boot image matches with the memory layout as it
// it's loaded at location 0x0. Therefore let's seek to the
// `next_comp_addr`.
writer.seek(SeekFrom::Start(next_comp_addr))?;
Ok(Self {
layout,
expert_mode: false,
comps: vec![],
writer,
builder,
legacy_se_hdr_addr,
stage3a,
stage3b,
finalized: false,
})
}
/// Enable expert mode - this is required for specifying component tweaks by
/// hand etc...
pub(crate) fn i_know_what_i_am_doing(&mut self) {
self.builder.i_know_what_i_am_doing();
self.expert_mode = true;
}
/// Prepare the given component as secured component, append it to the layout
/// and write it to the output.
///
/// # Errors
///
/// This function will return an error if:
/// + stage3b has already been added
/// + problem with the preparation of the secured component
/// + serialization problem of the component tweak (very unlikely)
/// + a tweak was given, but the expert mode not enabled
pub(crate) fn prepare_and_append_as_secure_component<T>(
&mut self,
component: &mut T,
tweak: Option<Vec<u8>>,
) -> Result<Rc<ImgComponent>>
where
T: ComponentTrait<ComponentKind>,
{
if self.finalized {
return Err(Error::ImgAlreadyFinalized.into());
}
if !component.secure_mode() {
unreachable!("Bug")
}
if tweak.is_some() && !self.expert_mode {
return Err(Error::NonExpertModeTweakGiven.into());
}
debug!("Preparing {} as secured component", component.kind());
let tweak = tweak.unwrap_or(serialize_to_bytes(&CompTweakV1::new(component.kind())?)?);
// No reason to seek as there are no holes between components (addr
// alignment == alignment of the component size). If that changes we have to seek beforehand
// to `self.layout.next_addr` self.writer.seek(SeekFrom::Start(self.layout.
// next_addr))?;
let secured_comp = self.builder.prepare_and_append_as_secure_component(
&mut self.writer,
&mut self.layout,
component,
tweak,
)?;
let img_comp = Rc::new(ImgComponent {
kind: component.kind(),
src: secured_comp.src.clone(),
secure_mode: Some(secured_comp),
});
self.comps.push(img_comp.clone());
Ok(img_comp)
}
/// Insert and write the given non-secured component at the given address.
fn insert_nonsecure_component<T: ComponentTrait<ComponentKind>>(
&mut self,
component: &mut T,
addr: u64,
) -> Result<Rc<ImgComponent>> {
// FIXME Guarantee this during compile time using a "SecureMode" trait.
if component.secure_mode() {
unreachable!("Programming bug!")
};
let max_component_size = self.layout.max_size_of_chunk_at_addr(addr)?;
let mut buf = vec![0_u8; self.builder.chunk_size()];
let mut total_written_count: usize = 0;
assert_ne!(buf.len(), 0);
self.writer.seek(SeekFrom::Start(addr))?;
loop {
let read_count = component.read(&mut buf)?;
// The end of file has reached as it's guaranteed that the buffer
// [`buf`] has a length != 0. See
// https://doc.rust-lang.org/std/io/trait.Read.html#tymethod.read
if read_count == 0 {
break;
}
if let Some(max_component_size) = max_component_size {
if total_written_count
.checked_add(read_count)
.ok_or(Error::UnexpectedOverflow)?
> max_component_size
{
return Err(anyhow!(
"BUG: Component is too large for this location in the image: {} > {}",
total_written_count + read_count,
max_component_size
));
}
}
self.writer.write_all(&buf[0..read_count])?;
total_written_count = total_written_count
.checked_add(read_count)
.ok_or(Error::UnexpectedOverflow)?;
}
let src = self
.layout
.insert_interval(addr, total_written_count.try_into()?)?;
let img_comp = Rc::new(ImgComponent {
src,
kind: component.kind(),
secure_mode: None,
});
match self.comps.binary_search(&img_comp) {
Ok(_pos) => {
return Err(anyhow!(
"BUG: There is already another component at this location"
))
}
Err(pos) => self.comps.insert(pos, img_comp.clone()),
}
Ok(img_comp)
}
fn append_component<T: ComponentTrait<ComponentKind>>(
&mut self,
component: &mut T,
) -> Result<Rc<ImgComponent>> {
let next_addr = self.layout.next_addr;
self.insert_nonsecure_component(component, next_addr)
}
/// Prepare IPIB and write it to file
fn add_ipib(&mut self, sehdr_src: &Interval) -> Result<Rc<ImgComponent>> {
let img_comps_tweak_and_src: Result<Vec<_>> = self
.comps
.iter()
.filter(|comp| comp.secure_mode.is_some())
.map(|comp| {
// Safety: We checked in the filter for `comp.secure_mode.is_some()`.
let secure_mode_data = comp.secure_mode.as_ref().unwrap();
let src = &comp.src;
let (_, tweak) = CompTweakV1::from_bytes((secure_mode_data.tweak(), 0))?;
Ok((tweak.pref, src.clone()))
})
.collect();
let ipib = create_ipib(sehdr_src, img_comps_tweak_and_src?)?;
let mut ipib_comp = Ipib::new(Box::new(Cursor::new(serialize_to_bytes(&ipib)?)));
self.append_component(&mut ipib_comp)
}
/// Prepare Secure Execution header and write it to the output
fn add_sehdr(&mut self, stage3b_entry: u64, sehdr_args: SeHdrArgs) -> Result<Rc<ImgComponent>> {
let meta = self.builder.finish()?;
let mut se_hdr_builder = SeHdrBuilder::new(
SeHdrVersion::V1,
PSW {
addr: sehdr_args.psw_addr.unwrap_or(stage3b_entry),
mask: Self::DEFAULT_INITIAL_PSW_MASK,
},
meta,
)?;
se_hdr_builder
.add_hostkeys(sehdr_args.keys)?
.with_pcf(sehdr_args.pcf)?
.with_scf(sehdr_args.scf)?;
if self.expert_mode {
se_hdr_builder.i_know_what_i_am_doing();
}
if let Some((path, cck)) = &sehdr_args.cck {
se_hdr_builder
.with_cck(cck.clone())
.with_context(|| format!("Failed to use '{}' as the CCK", path.display()))?;
}
if let Some((path, prot_key)) = sehdr_args.hdr_aead_key {
se_hdr_builder
.with_aead_key(prot_key.clone())
.with_context(|| {
format!(
"Failed to use '{}' as the Secure Execution header protection key",
path.display()
)
})?;
}
let se_hdr_bin = se_hdr_builder.build()?;
let mut comp: Component =
SeHdrComp::new(Box::new(Cursor::new(se_hdr_bin.as_bytes()?))).into();
if let Some(se_hdr_addr) = self.legacy_se_hdr_addr {
self.insert_nonsecure_component(&mut comp, se_hdr_addr)
} else {
self.append_component(&mut comp)
}
}
/// Finish the Secure Execution image - e.g. create Stage3a, Stage3b, Secure
/// Execution header and so on.
#[allow(clippy::similar_names)]
pub fn finish(mut self, sehdr_args: SeHdrArgs) -> Result<Vec<Rc<ImgComponent>>> {
if (sehdr_args.hdr_aead_key.is_some() || sehdr_args.psw_addr.is_some()) && !self.expert_mode
{
return Err(Error::NonExpertMode.into());
}
// Create stage3b and write it to the output file
let psw = PSW {
addr: S390Kernel::KERNEL_ENTRY,
mask: Self::DEFAULT_INITIAL_PSW_MASK,
};
let stage3b_img_comp = self
.add_stage3b(psw)
.context("Failed to prepare stage3b component")?;
// Create Secure Execution header and write it to the output file
let sehdr_img_comp = self
.add_sehdr(stage3b_img_comp.src.start, sehdr_args)
.context("Failed to prepare Secure Execution header")?;
// Create and write IPIB to the output file
let ipib_img_comp = self
.add_ipib(&sehdr_img_comp.src)
.context("Failed to prepare IPIB")?;
// Create and write stage3a to the output file
let stage3a_img_comp = self
.add_stage3a(&sehdr_img_comp.src, &ipib_img_comp.src)
.context("Failed to prepare Stage3a")?;
assert_eq!(stage3a_img_comp.src.start, STAGE3A_INIT_ENTRY);
assert_eq!(stage3a_img_comp.src.start + 0x1000, STAGE3A_ENTRY);
// Create and write short PSW at the beginning of the file
let _short_psw_img_comp = self.add_short_psw(
stage3a_img_comp
.src
.start
.checked_add(0x1000)
.ok_or(Error::UnexpectedOverflow)?,
)?;
Ok(self.comps)
}
/// Prepare stage3a and write it to file
fn add_stage3a(
&mut self,
se_hdr_src: &Interval,
ipib_src: &Interval,
) -> Result<Rc<ImgComponent>> {
let stage3a_load_addr = STAGE3A_LOAD_ADDRESS;
let mut stage3a_comp = render_stage3a(
self.stage3a.clone(),
stage3a_load_addr,
se_hdr_src,
ipib_src,
)?;
self.insert_nonsecure_component(&mut stage3a_comp, stage3a_load_addr)
}
/// Prepare short PSW and write it to file
fn add_short_psw(&mut self, stage3a_entry: u64) -> Result<Rc<ImgComponent>> {
let short_psw: ShortPsw = PSW {
addr: stage3a_entry,
mask: Self::DEFAULT_INITIAL_PSW_MASK,
}
.try_into()?;
let mut short_psw_comp =
ShortPSWComp::new(Box::new(Cursor::new(serialize_to_bytes(&short_psw)?)));
self.insert_nonsecure_component(&mut short_psw_comp, ShortPSWComp::OFFSET)
}
/// Prepare stage3b and write it to file
fn add_stage3b(&mut self, psw: PSW) -> Result<Rc<ImgComponent>> {
// Prepare stage3b - for this we must prepare the arguments for it. Since we
// have the memory layout for the movable components (kernel, cmdline, and
// initrd) we can do this now.
let mut stage3b_comp = render_stage3b(self.stage3b.clone(), psw, &self.comps)?;
let result = self.prepare_and_append_as_secure_component(&mut stage3b_comp, None);
// No other "regular components can be added now
self.finalized = true;
result
}
pub(crate) fn set_components_key(
&mut self,
key_data: Confidential<Vec<u8>>,
) -> pvimg::error::Result<()> {
self.builder.set_components_key(key_data)
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::SeImgBuilder;
use crate::{se_img::stage3a_path, se_img_comps::stage3b_path};
#[test]
fn test_comp_ctx_new() {
// If the bootloader does not exist, we cannot test.
if !stage3a_path(None).exists() || !stage3b_path(None).exists() {
return;
}
let encryption = true;
let mut writer = Cursor::new(Vec::new());
let ctx_res = SeImgBuilder::new_v1(&mut writer, encryption, None, None);
assert!(ctx_res.is_ok());
let ctx = ctx_res.unwrap();
assert_eq!(ctx.layout.next_addr, 0x13000);
assert!(ctx.builder.encryption_enabled());
assert_eq!(ctx.comps, vec![]);
}
}

View File

@@ -0,0 +1,374 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::{
fmt::{Debug, Display},
io::{Read, Seek, SeekFrom},
};
use anyhow::Context;
use deku::{ctx::Endian, DekuRead, DekuWrite};
use enum_dispatch::enum_dispatch;
use pv::request::random_array;
use pvimg::{error::Result, secured_comp::ComponentTrait};
use self::{
cmdline::Cmdline, kernel::S390Kernel, ramdisk::Ramdisk, sehdr::SeHdrComp,
shortpsw::ShortPSWComp, stage3a::Stage3a, stage3b::Stage3b,
};
pub use crate::se_img_comps::bootloader::{
create_ipib, render_stage3a, render_stage3b, stage3a_path, stage3b_path, STAGE3A_ENTRY,
STAGE3A_INIT_ENTRY, STAGE3A_LOAD_ADDRESS,
};
use crate::se_img_comps::ipib::Ipib;
mod bootloader;
pub mod cmdline;
pub mod ipib;
pub mod kernel;
pub mod ramdisk;
pub mod sehdr;
pub mod shortpsw;
pub mod stage3a;
pub mod stage3b;
/// A trait for checking a component.
#[enum_dispatch]
trait ComponentCheckTrait: ComponentTrait<ComponentKind> {
/// Check the component
///
/// Note: The implementer does not have to care about resetting the file position
/// as this is done by [`ComponentCheckCtx`].
fn check(&mut self, ctx: &ComponentCheckCtx) -> Result<()>;
/// Initialize [`ComponentCheckCtx`], e.g. it reads what max kernel command
/// line is supported by the given Linux kernel.
///
/// Note: The implementer does not have to care about resetting the file
/// position as this is done by the [`ComponentCheckCtx`]
fn init_ctx(&mut self, ctx: &mut ComponentCheckCtx) -> Result<()>;
}
#[derive(Debug)]
struct ComponentCheckCtx {
max_kernel_cmdline_size: usize,
}
impl Default for ComponentCheckCtx {
fn default() -> Self {
Self {
max_kernel_cmdline_size: S390Kernel::LEGACY_MAX_COMMAND_LINE_SIZE,
}
}
}
impl ComponentCheckCtx {
fn new() -> Self {
Default::default()
}
// Initialize component context.
fn init(&mut self, component: &mut Component) -> Result<()> {
let old_pos = component.stream_position()?;
let result = component.init_ctx(self);
component.seek(SeekFrom::Start(old_pos))?;
result
}
// Check component.
fn check_comp(&self, component: &mut Component) -> Result<()> {
let old_pos = component.stream_position()?;
let result = component.check(self);
component.seek(SeekFrom::Start(old_pos))?;
result
}
}
/// Check the given components.
///
/// The original stream position of the components remains as it was before
/// calling this function.
///
/// # Errors
///
/// This function will return an error if there was an IO error or the component
/// check has failed.
pub fn check_components(components: &mut [Component]) -> Result<(), anyhow::Error> {
let mut components_ctx = ComponentCheckCtx::new();
for component in components.iter_mut() {
components_ctx
.init(component)
.with_context(|| format!("Check for {} component has failed", component.kind()))?;
}
for component in components.iter_mut() {
components_ctx
.check_comp(component)
.with_context(|| format!("Check for {} component has failed", component.kind()))?;
}
Ok(())
}
#[non_exhaustive]
#[derive(Debug)]
#[enum_dispatch(ComponentCheckTrait)]
pub enum Component {
ShortPSW(ShortPSWComp),
Stage3a(Stage3a),
Kernel(S390Kernel),
Ramdisk(Ramdisk),
Cmdline(Cmdline),
Stage3b(Stage3b),
SeHdr(SeHdrComp),
Ipib(Ipib),
}
// No `enum_dispatch` can be used since the trait is implemented in another
// crate.
impl Seek for Component {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
match self {
Self::ShortPSW(obj) => obj.seek(pos),
Self::Stage3a(obj) => obj.seek(pos),
Self::Kernel(obj) => obj.seek(pos),
Self::Ramdisk(obj) => obj.seek(pos),
Self::Cmdline(obj) => obj.seek(pos),
Self::Stage3b(obj) => obj.seek(pos),
Self::SeHdr(obj) => obj.seek(pos),
Self::Ipib(obj) => obj.seek(pos),
}
}
}
// No `enum_dispatch` can be used since the trait is implemented in another
// crate.
impl Read for Component {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::ShortPSW(obj) => obj.read(buf),
Self::Stage3a(obj) => obj.read(buf),
Self::Kernel(obj) => obj.read(buf),
Self::Ramdisk(obj) => obj.read(buf),
Self::Cmdline(obj) => obj.read(buf),
Self::Stage3b(obj) => obj.read(buf),
Self::SeHdr(obj) => obj.read(buf),
Self::Ipib(obj) => obj.read(buf),
}
}
}
// No `enum_dispatch` can be used since the trait is implemented in another
// crate.
impl ComponentTrait<ComponentKind> for Component {
fn secure_mode(&self) -> bool {
match self {
Self::ShortPSW(obj) => obj.secure_mode(),
Self::Stage3a(obj) => obj.secure_mode(),
Self::Kernel(obj) => obj.secure_mode(),
Self::Ramdisk(obj) => obj.secure_mode(),
Self::Cmdline(obj) => obj.secure_mode(),
Self::Stage3b(obj) => obj.secure_mode(),
Self::SeHdr(obj) => obj.secure_mode(),
Self::Ipib(obj) => obj.secure_mode(),
}
}
fn kind(&self) -> ComponentKind {
match self {
Self::ShortPSW(obj) => obj.kind(),
Self::Stage3a(obj) => obj.kind(),
Self::Kernel(obj) => obj.kind(),
Self::Ramdisk(obj) => obj.kind(),
Self::Cmdline(obj) => obj.kind(),
Self::Stage3b(obj) => obj.kind(),
Self::SeHdr(obj) => obj.kind(),
Self::Ipib(obj) => obj.kind(),
}
}
}
// Trick to be able to pass it as `&dyn ReadSeekDebug`
pub trait ReadSeekDebug: Read + Seek + Debug {}
impl<T: Read + Seek + Debug> ReadSeekDebug for T {}
#[derive(Debug)]
pub struct CompReader {
reader: Box<dyn ReadSeekDebug>,
}
impl CompReader {
pub fn new(reader: Box<dyn ReadSeekDebug>) -> Self {
Self { reader }
}
}
impl Read for CompReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.reader.read(buf)
}
}
impl Seek for CompReader {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
self.reader.seek(pos)
}
}
/// The order of enum variants implicitly defines the order of the secured
/// components within the Secure Execution image!
#[repr(u16)]
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq)]
pub enum ComponentKind {
ShortPSW = 10,
Stage3a = 30,
Kernel = 40,
Ramdisk = 50,
Cmdline = 60,
Stage3b = 70,
SeHdr = 80,
Ipib = 90,
}
impl ComponentKind {
pub fn tweak_prefix(&self) -> u16 {
self.clone() as u16
}
pub fn from_tweak_prefix(value: u16) -> Self {
// Safety: `value` must correspond to a discriminant value of `Self`
unsafe { std::mem::transmute(value) }
}
}
impl Display for ComponentKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(
&match self {
Self::Kernel => "Linux kernel",
Self::Ramdisk => "ramdisk",
Self::Cmdline => "kernel cmdline",
Self::Stage3a => "stage3a",
Self::Stage3b => "stage3b",
Self::SeHdr => "Secure Execution header",
Self::Ipib => "IPIB",
Self::ShortPSW => "short PSW",
}
.to_string(),
f,
)
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone, DekuRead, DekuWrite)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct CompTweakPrefV1 {
pub comp_prefix: u16,
pub rand: [u8; 6],
}
impl CompTweakPrefV1 {
fn to_u64(&self) -> u64 {
let mut bytes_be = self.comp_prefix.to_be_bytes().to_vec();
bytes_be.extend_from_slice(self.rand.as_slice());
assert_eq!(bytes_be.len(), 8);
// Safety: `bytes_be ` is guaranteed to be 8 bytes long.
u64::from_be_bytes(bytes_be.try_into().unwrap())
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone, DekuRead, DekuWrite)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct CompTweakV1 {
pub pref: CompTweakPrefV1,
pub pg_idx: u64,
}
impl CompTweakV1 {
pub fn new(kind: ComponentKind) -> Result<Self> {
let pref = CompTweakPrefV1 {
comp_prefix: kind.tweak_prefix(),
rand: random_array()?,
};
Ok(Self { pref, pg_idx: 0 })
}
pub const fn comp_prefix(&self) -> u16 {
self.pref.comp_prefix
}
}
#[allow(clippy::shadow_unrelated)]
#[cfg(test)]
mod tests {
use deku::{DekuContainerRead, DekuContainerWrite};
use proptest::{
prelude::{Just, Strategy},
prop_assert_eq, prop_oneof, proptest,
};
use super::{ComponentCheckCtx, ComponentKind};
use crate::se_img_comps::{check_components, kernel::S390Kernel, CompTweakPrefV1, CompTweakV1};
fn component_kind_strategy() -> impl Strategy<Value = ComponentKind> {
prop_oneof![
Just(ComponentKind::ShortPSW),
Just(ComponentKind::Stage3a),
Just(ComponentKind::Kernel),
Just(ComponentKind::Ramdisk),
Just(ComponentKind::Cmdline),
Just(ComponentKind::Stage3b),
Just(ComponentKind::SeHdr),
Just(ComponentKind::Ipib),
]
}
proptest! {
#[test]
fn tweak_prefix_back_to_original(kind in component_kind_strategy()) {
let prefix = kind.tweak_prefix();
prop_assert_eq!(kind, ComponentKind::from_tweak_prefix(prefix));
}
}
#[test]
fn compctx() {
let ctx = ComponentCheckCtx::new();
assert_eq!(
ctx.max_kernel_cmdline_size,
S390Kernel::LEGACY_MAX_COMMAND_LINE_SIZE
);
}
#[test]
fn test_check_components() {
check_components(&mut []).unwrap();
}
#[test]
fn comptweak_v1() {
let tweak = CompTweakV1 {
pref: CompTweakPrefV1 {
comp_prefix: 3,
rand: [157, 239, 44, 103, 219, 118],
},
pg_idx: 0,
};
let bytes = [0, 3, 157, 239, 44, 103, 219, 118, 0, 0, 0, 0, 0, 0, 0, 0];
assert_eq!(tweak.pref.to_u64(), 1018075497880438);
assert_eq!(tweak.to_bytes().unwrap(), bytes,);
assert_eq!(CompTweakV1::from_bytes((&bytes, 0)).unwrap().1, tweak);
let tweak = CompTweakV1 {
pref: CompTweakPrefV1 {
comp_prefix: 0,
rand: [0; 6],
},
pg_idx: 0,
};
let bytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
assert_eq!(tweak.pref.to_u64(), 0);
assert_eq!(tweak.to_bytes().unwrap(), bytes,);
assert_eq!(CompTweakV1::from_bytes((&bytes, 0)).unwrap().1, tweak);
}
}

View File

@@ -0,0 +1,216 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::{io::Cursor, path::PathBuf, rc::Rc};
pub mod ipl;
mod stage3a_defs;
mod stage3b_defs;
use ipl::IPL_PARM_BLOCK_PV_VERSION;
use log::trace;
use pvimg::{
error::{Error, Result},
misc::{serialize_to_bytes, PSW},
secured_comp::Interval,
};
pub use self::stage3a_defs::{STAGE3A_ENTRY, STAGE3A_INIT_ENTRY, STAGE3A_LOAD_ADDRESS};
use self::{
ipl::{
ipl_parameter_block, ipl_pb0_pv, ipl_pb0_pv_comp, ipl_pbt_IPL_PBT_PV, ipl_pl_hdr,
IPL_PARM_BLOCK_VERSION,
},
stage3b_defs::{memblob, stage3b_args},
};
use super::CompTweakPrefV1;
use crate::{
se_img::ImgComponent,
se_img_comps::{
bootloader::stage3a_defs::stage3a_args, stage3a::Stage3a, stage3b::Stage3b, ComponentKind,
},
};
/// Get the `PVIMG_PKGDATADIR` used for `pvimg`
///
/// Provides the package data directory for `pvimg`.
/// For release builds this requires the environment variable
/// `PVIMG_PKGDATADIR` to be present at compile time.
/// For debug builds this value defaults to `CARGO_MANIFEST_DIR/boot/`
/// if that variable is not present.
/// Should only be used by binary targets!!
///
/// Collapses to a compile time constant, that is likely to be inlined by the
/// compiler in release builds.
macro_rules! pvimg_pkg_data {
() => {{
#[cfg(debug_assertions)]
match option_env!("PVIMG_PKGDATADIR") {
Some(data) => data,
None => concat!(env!("CARGO_MANIFEST_DIR"), "/boot/"),
}
#[cfg(not(debug_assertions))]
env!("PVIMG_PKGDATADIR", "env 'PVIMG_PKGDATADIR' must be set for release builds. Trigger build using the s390-tools build system or export the variable yourself")
}};
}
fn bootloader_dir(path: Option<&PathBuf>) -> PathBuf {
path.map_or_else(|| PathBuf::from(pvimg_pkg_data!()), |v| v.to_owned())
}
/// Returns the path to `stage3a.bin`.
pub fn stage3a_path(dir: Option<&PathBuf>) -> PathBuf {
bootloader_dir(dir).join("stage3a.bin")
}
/// Returns the path to `stage3b_reloc.bin`.
pub fn stage3b_path(dir: Option<&PathBuf>) -> PathBuf {
bootloader_dir(dir).join("stage3b_reloc.bin")
}
/// Render stage3b "template"
pub fn render_stage3a(
mut stage3a: Vec<u8>,
stage3a_addr: u64,
se_hdr_src: &Interval,
ipib_src: &Interval,
) -> Result<Stage3a> {
let stage3a_size = stage3a.len();
let stage3a_size_u64: u64 = stage3a_size.try_into()?;
if stage3a_size < 24 {
unreachable!("Bug!");
}
let stage3a_data_addr = stage3a_addr
.checked_add(stage3a_size_u64)
.ok_or(Error::UnexpectedOverflow)?
- 24;
assert!(
se_hdr_src.start
> stage3a_addr
.checked_add(stage3a_size_u64)
.ok_or(Error::UnexpectedOverflow)?
);
// IMPORTANT: Secure Execution header must be located AFTER the stage3a
// loader.
let hdr_offs = se_hdr_src
.start
.checked_sub(stage3a_data_addr)
.ok_or(Error::UnexpectedUnderflow)?;
assert!(
ipib_src.start
> stage3a_addr
.checked_add(stage3a_size_u64)
.ok_or(Error::UnexpectedOverflow)?
);
// IMPORTANT: IPIB must be located AFTER the stage3a loader.
let ipib_offs = ipib_src
.start
.checked_sub(stage3a_data_addr)
.ok_or(Error::UnexpectedUnderflow)?;
let args = stage3a_args {
hdr_offs,
hdr_size: se_hdr_src.size(),
ipib_offs,
};
trace!("stage3a arguments: {args:#x?}");
let stage3a_args_bin = serialize_to_bytes(&args)?;
assert_eq!(stage3a_args_bin.len(), 24);
// Insert the stage3a arguments
assert!(stage3a_size > stage3a_args_bin.len());
stage3a.splice(stage3a_size - stage3a_args_bin.len().., stage3a_args_bin);
Ok(Stage3a::new(Box::new(Cursor::new(stage3a))))
}
/// Render stage3b "template"
pub fn render_stage3b(
mut stage3b: Vec<u8>,
psw: PSW,
prepared_comps: &[Rc<ImgComponent>],
) -> Result<Stage3b> {
let mut args = stage3b_args {
psw,
..Default::default()
};
prepared_comps
.iter()
.filter(|comp| comp.secure_mode.is_some() && comp.kind() != ComponentKind::Stage3b)
.map(|comp| {
// Safety: Safe because of the filtering.
let secure_mode_data = comp.secure_mode.as_ref().unwrap();
let src = comp.src.start;
let size = secure_mode_data.original_size.try_into()?;
match comp.kind() {
ComponentKind::Cmdline => args.cmdline = memblob { src, size },
ComponentKind::Kernel => args.kernel = memblob { src, size },
ComponentKind::Ramdisk => args.initrd = memblob { src, size },
ComponentKind::Stage3a
| ComponentKind::Ipib
| ComponentKind::SeHdr
| ComponentKind::ShortPSW
| ComponentKind::Stage3b => unreachable!(),
}
Ok(())
})
.collect::<Result<Vec<_>>>()?;
if prepared_comps.len() > 3 {
// That would mean there is a bug somewhere.
unreachable!()
}
trace!("stage3b arguments: {args:#x?}");
let stage3b_args_bin = serialize_to_bytes(&args)?;
assert_eq!(stage3b_args_bin.len(), 64);
let stage3b_len = stage3b.len();
let stage3b_args_bin_len = stage3b_args_bin.len();
// Insert the stage3b arguments
assert!(stage3b_len > stage3b_args_bin_len);
let stage3b_parms_off = stage3b_len - stage3b_args_bin_len;
stage3b.splice(stage3b_parms_off.., stage3b_args_bin);
Ok(Stage3b::new(Box::new(Cursor::new(stage3b))))
}
pub fn create_ipib(
hdr: &Interval,
img_comps: Vec<(CompTweakPrefV1, Rc<Interval>)>,
) -> Result<ipl_parameter_block> {
let mut components = vec![];
for (tweak_pref, src) in img_comps {
components.push(ipl_pb0_pv_comp {
tweak_pref: tweak_pref.to_u64(),
addr: src.start,
len: src.size(),
});
}
let comps_len = components.len();
let ipip_len = ipl_parameter_block::size(comps_len)?.try_into()?;
let ipip_pv_len = ipl_pb0_pv::size(comps_len)?.try_into()?;
let ipib = ipl_parameter_block {
hdr: ipl_pl_hdr {
len: ipip_len,
flags: 0,
version: IPL_PARM_BLOCK_VERSION,
..Default::default()
},
pv: ipl_pb0_pv {
len: ipip_pv_len,
pbt: ipl_pbt_IPL_PBT_PV,
version: IPL_PARM_BLOCK_PV_VERSION,
num_comp: comps_len.try_into()?,
pv_hdr_addr: hdr.start,
pv_hdr_size: hdr.size(),
components,
..Default::default()
},
};
Ok(ipib)
}

View File

@@ -0,0 +1,118 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
// Based on the output of rust-bindgen 0.69.1
#![allow(nonstandard_style, unused)]
use deku::{ctx::Endian, prelude::*};
use pvimg::{error::Result, misc::bytesize};
pub const IPL_FLAG_SECURE: u32 = 64;
pub const IPL_RB_COMPONENT_FLAG_SIGNED: u32 = 128;
pub const IPL_RB_COMPONENT_FLAG_VERIFIED: u32 = 64;
pub const IPL_MAX_SUPPORTED_VERSION: u32 = 0;
pub const IPL_PARM_BLOCK_VERSION: u8 = 1;
pub const IPL_PARM_BLOCK_PV_VERSION: u8 = 1;
pub const ipl_pbt_IPL_PBT_FCP: ipl_pbt = 0;
pub const ipl_pbt_IPL_PBT_SCP_DATA: ipl_pbt = 1;
pub const ipl_pbt_IPL_PBT_CCW: ipl_pbt = 2;
pub const ipl_pbt_IPL_PBT_ECKD: ipl_pbt = 3;
pub const ipl_pbt_IPL_PBT_NVME: ipl_pbt = 4;
pub const ipl_pbt_IPL_PBT_PV: ipl_pbt = 5;
pub type ipl_pbt = u8;
#[repr(C)]
#[derive(Debug, Default, Clone, DekuRead, DekuWrite)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct ipl_pl_hdr {
pub len: u32,
pub flags: u8,
pub reserved1: [u8; 2_usize],
pub version: u8,
}
#[repr(C)]
#[derive(Debug, Default, Clone, DekuRead, DekuWrite)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct ipl_pb0_pv_comp {
pub tweak_pref: u64,
pub addr: u64,
pub len: u64,
}
#[repr(C)]
#[derive(Debug, Clone, DekuRead, DekuWrite)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct ipl_pb0_pv {
pub len: u32,
pub pbt: u8,
pub reserved1: [u8; 3_usize],
pub loadparm: [u8; 8_usize],
pub reserved2: [u8; 84_usize],
pub reserved3: [u8; 3_usize],
pub version: u8,
pub reserved4: [u8; 4_usize],
pub num_comp: u32,
pub pv_hdr_addr: u64,
pub pv_hdr_size: u64,
#[deku(count = "num_comp")]
pub components: Vec<ipl_pb0_pv_comp>,
}
impl Default for ipl_pb0_pv {
fn default() -> Self {
Self {
len: Default::default(),
pbt: Default::default(),
reserved1: Default::default(),
loadparm: Default::default(),
reserved2: [0; 84],
reserved3: Default::default(),
version: Default::default(),
reserved4: Default::default(),
num_comp: Default::default(),
pv_hdr_addr: Default::default(),
pv_hdr_size: Default::default(),
components: Default::default(),
}
}
}
#[repr(C)]
#[derive(Debug, Default, Clone, DekuRead, DekuWrite)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct ipl_parameter_block {
pub hdr: ipl_pl_hdr,
pub pv: ipl_pb0_pv,
}
use std::iter;
impl ipl_parameter_block {
pub fn size(num_comp: usize) -> Result<usize> {
let comps = iter::repeat(ipl_pb0_pv_comp::default())
.take(num_comp)
.collect();
let ipib = Self {
pv: ipl_pb0_pv {
components: comps,
..Default::default()
},
..Default::default()
};
bytesize(&ipib)
}
}
impl ipl_pb0_pv {
pub fn size(num_comp: usize) -> Result<usize> {
let comp = ipl_pb0_pv_comp::default();
let comps = iter::repeat(comp).take(num_comp).collect();
let ipl = Self {
components: comps,
..Default::default()
};
bytesize(&ipl)
}
}

View File

@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
// Based on the output of rust-bindgen 0.69.1
#![allow(nonstandard_style)]
use deku::{ctx::Endian, prelude::*};
pub const IMAGE_ENTRY: u64 = 0x10000;
pub const STAGE3A_INIT_ENTRY: u64 = IMAGE_ENTRY;
pub const STAGE3A_ENTRY: u64 = STAGE3A_INIT_ENTRY + 0x1000;
pub const STAGE3A_LOAD_ADDRESS: u64 = STAGE3A_INIT_ENTRY;
pub const STAGE3A_BSS_ADDRESS: u64 = 0xc000;
pub const STAGE3A_BSS_SIZE: u64 = 0x1000;
#[derive(Debug, Default, Clone, DekuRead, DekuWrite)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct stage3a_args {
pub hdr_offs: u64,
pub hdr_size: u64,
pub ipib_offs: u64,
}
#[test]
fn bindgen_test_layout_stage3a_args() {
const UNINIT: ::std::mem::MaybeUninit<stage3a_args> = ::std::mem::MaybeUninit::uninit();
let ptr = UNINIT.as_ptr();
assert_eq!(
::std::mem::size_of::<stage3a_args>(),
24_usize,
concat!("Size of: ", stringify!(stage3a_args))
);
assert_eq!(
::std::mem::align_of::<stage3a_args>(),
8_usize,
concat!("Alignment of ", stringify!(stage3a_args))
);
assert_eq!(
unsafe { ::std::ptr::addr_of!((*ptr).hdr_offs) as usize - ptr as usize },
0_usize,
concat!(
"Offset of field: ",
stringify!(stage3a_args),
"::",
stringify!(hdr_offs)
)
);
assert_eq!(
unsafe { ::std::ptr::addr_of!((*ptr).hdr_size) as usize - ptr as usize },
8_usize,
concat!(
"Offset of field: ",
stringify!(stage3a_args),
"::",
stringify!(hdr_size)
)
);
assert_eq!(
unsafe { ::std::ptr::addr_of!((*ptr).ipib_offs) as usize - ptr as usize },
16_usize,
concat!(
"Offset of field: ",
stringify!(stage3a_args),
"::",
stringify!(ipib_offs)
)
);
}

View File

@@ -0,0 +1,116 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
// Based on the output of rust-bindgen 0.69.1
#![allow(non_camel_case_types, non_snake_case, nonstandard_style)]
use deku::{ctx::Endian, prelude::*};
use pvimg::misc::PSW;
#[derive(Debug, Default, Clone, DekuRead, DekuWrite)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct memblob {
pub src: u64,
pub size: u64,
}
#[test]
fn bindgen_test_layout_memblob() {
const UNINIT: ::std::mem::MaybeUninit<memblob> = ::std::mem::MaybeUninit::uninit();
let ptr = UNINIT.as_ptr();
assert_eq!(
::std::mem::size_of::<memblob>(),
16_usize,
concat!("Size of: ", stringify!(memblob))
);
assert_eq!(
::std::mem::align_of::<memblob>(),
8_usize,
concat!("Alignment of ", stringify!(memblob))
);
assert_eq!(
unsafe { ::std::ptr::addr_of!((*ptr).src) as usize - ptr as usize },
0_usize,
concat!(
"Offset of field: ",
stringify!(memblob),
"::",
stringify!(src)
)
);
assert_eq!(
unsafe { ::std::ptr::addr_of!((*ptr).size) as usize - ptr as usize },
8_usize,
concat!(
"Offset of field: ",
stringify!(memblob),
"::",
stringify!(size)
)
);
}
#[derive(Debug, Default, Clone, DekuRead, DekuWrite)]
#[deku(endian = "endian", ctx = "endian: Endian", ctx_default = "Endian::Big")]
pub struct stage3b_args {
pub kernel: memblob,
pub cmdline: memblob,
pub initrd: memblob,
pub psw: PSW,
}
#[test]
fn bindgen_test_layout_stage3b_args() {
const UNINIT: ::std::mem::MaybeUninit<stage3b_args> = ::std::mem::MaybeUninit::uninit();
let ptr = UNINIT.as_ptr();
assert_eq!(
::std::mem::size_of::<stage3b_args>(),
64_usize,
concat!("Size of: ", stringify!(stage3b_args))
);
assert_eq!(
::std::mem::align_of::<stage3b_args>(),
8_usize,
concat!("Alignment of ", stringify!(stage3b_args))
);
assert_eq!(
unsafe { ::std::ptr::addr_of!((*ptr).kernel) as usize - ptr as usize },
0_usize,
concat!(
"Offset of field: ",
stringify!(stage3b_args),
"::",
stringify!(kernel)
)
);
assert_eq!(
unsafe { ::std::ptr::addr_of!((*ptr).cmdline) as usize - ptr as usize },
16_usize,
concat!(
"Offset of field: ",
stringify!(stage3b_args),
"::",
stringify!(cmdline)
)
);
assert_eq!(
unsafe { ::std::ptr::addr_of!((*ptr).initrd) as usize - ptr as usize },
32_usize,
concat!(
"Offset of field: ",
stringify!(stage3b_args),
"::",
stringify!(initrd)
)
);
assert_eq!(
unsafe { ::std::ptr::addr_of!((*ptr).psw) as usize - ptr as usize },
48_usize,
concat!(
"Offset of field: ",
stringify!(stage3b_args),
"::",
stringify!(psw)
)
);
}

View File

@@ -0,0 +1,91 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::io::{Read, Seek, SeekFrom};
use pvimg::error::{Error, Result};
use super::{
CompReader, ComponentCheckCtx, ComponentCheckTrait, ComponentKind, ComponentTrait,
ReadSeekDebug,
};
#[derive(Debug)]
pub struct Cmdline {
comp: CompReader,
last_value: Option<u8>,
}
impl Cmdline {
pub fn new(reader: Box<dyn ReadSeekDebug>) -> Self {
Self {
comp: CompReader { reader },
last_value: None,
}
}
}
impl Read for Cmdline {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
// Make sure that the kernel cmdline always is C NUL-terminated.
let size = self.comp.read(buf)?;
// Store last value
if size > 0 {
self.last_value = Some(buf[size - 1]);
return Ok(size);
}
if buf.is_empty() {
return Ok(size);
}
// EOF has been reached, check for NUL-Terminator
assert!(size == 0);
// Was the last value a NUL-Terminator?
if self.last_value.is_some_and(|x| x == b'\0') {
return Ok(size);
}
// Store a NUL-Terminator in buf so the next `read(...)` call will stop.
buf[0] = b'\0';
self.last_value = Some(buf[0]);
Ok(1)
}
}
impl Seek for Cmdline {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
// Invalidate last value after seeking
self.last_value = None;
self.comp.seek(pos)
}
}
impl ComponentCheckTrait for Cmdline {
fn check(&mut self, ctx: &ComponentCheckCtx) -> Result<()> {
let mut buf = vec![];
let size = self.read_to_end(&mut buf)?;
if size > ctx.max_kernel_cmdline_size {
return Err(Error::KernelCmdlineTooLarge {
size,
max_size: ctx.max_kernel_cmdline_size,
});
}
Ok(())
}
fn init_ctx(&mut self, _ctx: &mut ComponentCheckCtx) -> Result<()> {
Ok(())
}
}
impl ComponentTrait<ComponentKind> for Cmdline {
fn kind(&self) -> ComponentKind {
ComponentKind::Cmdline
}
fn secure_mode(&self) -> bool {
true
}
}

View File

@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::io::{Read, Seek};
use pvimg::error::Result;
use pvimg::secured_comp::ComponentTrait;
use super::ComponentKind;
use super::{CompReader, ComponentCheckCtx, ComponentCheckTrait, ReadSeekDebug};
#[derive(Debug)]
pub struct Ipib(CompReader);
impl Ipib {
pub fn new(reader: Box<dyn ReadSeekDebug>) -> Self {
Self(CompReader { reader })
}
}
impl Read for Ipib {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl Seek for Ipib {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.0.seek(pos)
}
}
impl ComponentCheckTrait for Ipib {
fn check(&mut self, _ctx: &ComponentCheckCtx) -> Result<()> {
Ok(())
}
fn init_ctx(&mut self, _ctx: &mut ComponentCheckCtx) -> Result<()> {
Ok(())
}
}
impl ComponentTrait<ComponentKind> for Ipib {
fn secure_mode(&self) -> bool {
false
}
fn kind(&self) -> ComponentKind {
ComponentKind::Ipib
}
}

View File

@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::io::{Read, Seek, SeekFrom};
use pvimg::error::{Error, Result};
use super::{
CompReader, ComponentCheckCtx, ComponentCheckTrait, ComponentKind, ComponentTrait,
ReadSeekDebug,
};
#[derive(Debug)]
pub struct S390Kernel(CompReader);
impl S390Kernel {
const ELF_MAGIC: [u8; Self::ELF_MAGIC_SIZE] = [0x7f, 0x45, 0x4c, 0x46];
const ELF_MAGIC_OFF: u64 = 0x0;
const ELF_MAGIC_SIZE: usize = 4;
const KERNEL_COMMAND_LINE_SIZE_ADDR: u64 = 0x10430;
const KERNEL_COMMAND_LINE_SIZE_LEN: usize = 8;
pub const KERNEL_ENTRY: u64 = 0x10000;
pub const LEGACY_MAX_COMMAND_LINE_SIZE: usize = 896;
const S390EP: [u8; Self::S390EP_SIZE] = [0x53, 0x33, 0x39, 0x30, 0x45, 0x50];
// Location of "S390EP" in a Linux binary (see arch/s390/boot/head.S)
const S390EP_OFFS: u64 = 0x10008;
const S390EP_SIZE: usize = 6;
pub fn new(reader: Box<dyn ReadSeekDebug>) -> Self {
Self(CompReader { reader })
}
fn is_elf_file(&mut self) -> Result<bool> {
self.seek(SeekFrom::Start(Self::ELF_MAGIC_OFF))?;
let mut buf = [0x0_u8; Self::ELF_MAGIC_SIZE];
self.read_exact(&mut buf)?;
Ok(buf == Self::ELF_MAGIC)
}
fn is_s390x_kernel(&mut self) -> Result<bool> {
self.seek(SeekFrom::Start(Self::S390EP_OFFS))?;
let mut buf = [0_u8; Self::S390EP_SIZE];
self.read_exact(&mut buf)?;
Ok(buf == Self::S390EP)
}
fn read_max_kernel_cmdline_size(&mut self) -> Result<usize> {
self.seek(SeekFrom::Start(Self::KERNEL_COMMAND_LINE_SIZE_ADDR))?;
let mut buf = [0x0_u8; Self::KERNEL_COMMAND_LINE_SIZE_LEN];
self.read_exact(&mut buf).map_err(|e| match e.kind() {
std::io::ErrorKind::UnexpectedEof => Error::NoS390Kernel,
_ => e.into(),
})?;
let mut max_size = u64::from_be_bytes(buf).try_into()?;
if max_size == 0 {
max_size = Self::LEGACY_MAX_COMMAND_LINE_SIZE;
}
Ok(max_size)
}
}
impl Read for S390Kernel {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl Seek for S390Kernel {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
self.0.seek(pos)
}
}
impl ComponentCheckTrait for S390Kernel {
fn check(&mut self, _ctx: &ComponentCheckCtx) -> Result<()> {
if self.is_elf_file()? {
return Err(Error::UnexpectedElfFile);
}
if !self.is_s390x_kernel()? {
return Err(Error::NoS390Kernel);
}
Ok(())
}
fn init_ctx(&mut self, ctx: &mut ComponentCheckCtx) -> Result<()> {
ctx.max_kernel_cmdline_size = self.read_max_kernel_cmdline_size()?;
Ok(())
}
}
impl ComponentTrait<ComponentKind> for S390Kernel {
fn secure_mode(&self) -> bool {
true
}
fn kind(&self) -> ComponentKind {
ComponentKind::Kernel
}
}

View File

@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::io::{Read, Seek};
use pvimg::error::Result;
use super::ComponentKind;
use super::{CompReader, ComponentCheckCtx, ComponentCheckTrait, ComponentTrait, ReadSeekDebug};
#[derive(Debug)]
pub struct Ramdisk(CompReader);
impl Ramdisk {
pub fn new(reader: Box<dyn ReadSeekDebug>) -> Self {
Self(CompReader { reader })
}
}
impl Read for Ramdisk {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl Seek for Ramdisk {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.0.seek(pos)
}
}
impl ComponentCheckTrait for Ramdisk {
fn check(&mut self, _ctx: &ComponentCheckCtx) -> Result<()> {
Ok(())
}
fn init_ctx(&mut self, _ctx: &mut ComponentCheckCtx) -> Result<()> {
Ok(())
}
}
impl ComponentTrait<ComponentKind> for Ramdisk {
fn kind(&self) -> ComponentKind {
ComponentKind::Ramdisk
}
fn secure_mode(&self) -> bool {
true
}
}

View File

@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::io::{Read, Seek};
use pvimg::error::Result;
use pvimg::secured_comp::ComponentTrait;
use super::ComponentKind;
use super::{CompReader, ComponentCheckCtx, ComponentCheckTrait, ReadSeekDebug};
#[derive(Debug)]
pub struct SeHdrComp(pub CompReader);
impl SeHdrComp {
pub fn new(reader: Box<dyn ReadSeekDebug>) -> Self {
Self(CompReader { reader })
}
}
impl Seek for SeHdrComp {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.0.seek(pos)
}
}
impl Read for SeHdrComp {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl ComponentCheckTrait for SeHdrComp {
fn check(&mut self, _ctx: &ComponentCheckCtx) -> Result<()> {
Ok(())
}
fn init_ctx(&mut self, _ctx: &mut ComponentCheckCtx) -> Result<()> {
Ok(())
}
}
impl ComponentTrait<ComponentKind> for SeHdrComp {
fn kind(&self) -> ComponentKind {
ComponentKind::SeHdr
}
fn secure_mode(&self) -> bool {
false
}
}

View File

@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::io::{Read, Seek};
use pvimg::error::Result;
use pvimg::secured_comp::ComponentTrait;
use super::ComponentKind;
use super::{CompReader, ComponentCheckCtx, ComponentCheckTrait, ReadSeekDebug};
#[derive(Debug)]
pub struct ShortPSWComp(CompReader);
impl ShortPSWComp {
/// Offset in the Secure Execution image
pub const OFFSET: u64 = 0x0;
pub fn new(reader: Box<dyn ReadSeekDebug>) -> Self {
Self(CompReader { reader })
}
}
impl Read for ShortPSWComp {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl Seek for ShortPSWComp {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.0.seek(pos)
}
}
impl ComponentCheckTrait for ShortPSWComp {
fn check(&mut self, _ctx: &ComponentCheckCtx) -> Result<()> {
Ok(())
}
fn init_ctx(&mut self, _ctx: &mut ComponentCheckCtx) -> Result<()> {
Ok(())
}
}
impl ComponentTrait<ComponentKind> for ShortPSWComp {
fn kind(&self) -> ComponentKind {
ComponentKind::ShortPSW
}
fn secure_mode(&self) -> bool {
false
}
}

View File

@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::io::{Read, Seek};
use pvimg::error::Result;
use pvimg::secured_comp::ComponentTrait;
use super::ComponentKind;
use super::{CompReader, ComponentCheckCtx, ComponentCheckTrait, ReadSeekDebug};
#[derive(Debug)]
pub struct Stage3a(CompReader);
impl Stage3a {
pub fn new(reader: Box<dyn ReadSeekDebug>) -> Self {
Self(CompReader { reader })
}
}
impl Read for Stage3a {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl Seek for Stage3a {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.0.seek(pos)
}
}
impl ComponentCheckTrait for Stage3a {
fn check(&mut self, _ctx: &ComponentCheckCtx) -> Result<()> {
Ok(())
}
fn init_ctx(&mut self, _ctx: &mut ComponentCheckCtx) -> Result<()> {
Ok(())
}
}
impl ComponentTrait<ComponentKind> for Stage3a {
fn kind(&self) -> ComponentKind {
ComponentKind::Stage3a
}
fn secure_mode(&self) -> bool {
false
}
}

View File

@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2024
use std::io::{Read, Seek};
use pvimg::error::Result;
use pvimg::secured_comp::ComponentTrait;
use super::ComponentKind;
use super::{CompReader, ComponentCheckCtx, ComponentCheckTrait, ReadSeekDebug};
#[derive(Debug)]
pub struct Stage3b(CompReader);
impl Stage3b {
pub fn new(reader: Box<dyn ReadSeekDebug>) -> Self {
Self(CompReader::new(reader))
}
}
impl Read for Stage3b {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl Seek for Stage3b {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.0.seek(pos)
}
}
impl ComponentCheckTrait for Stage3b {
fn check(&mut self, _ctx: &ComponentCheckCtx) -> Result<()> {
Ok(())
}
fn init_ctx(&mut self, _ctx: &mut ComponentCheckCtx) -> Result<()> {
Ok(())
}
}
impl ComponentTrait<ComponentKind> for Stage3b {
fn kind(&self) -> ComponentKind {
ComponentKind::Stage3b
}
fn secure_mode(&self) -> bool {
true
}
}

View File

@@ -18,7 +18,7 @@ use std::path::{Path, PathBuf};
use std::process::ExitCode;
/// CLI Argument collection for handling host-keys, IBM signing keys, and certificates.
#[derive(Args, Debug, PartialEq, Eq, Default)]
#[derive(Args, Debug, Clone, PartialEq, Eq, Default)]
#[command(
group(ArgGroup::new("pv_verify").required(true).args(["no_verify", "certs"])),
)]