diff --git a/README.md b/README.md index 161365c8..40d09d62 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/rust/Makefile b/rust/Makefile index 3ef8d862..1a4f0059 100644 --- a/rust/Makefile +++ b/rust/Makefile @@ -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) diff --git a/rust/pvimg/README.md b/rust/pvimg/README.md new file mode 100644 index 00000000..9ed52028 --- /dev/null +++ b/rust/pvimg/README.md @@ -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 | diff --git a/rust/pvimg/build.rs b/rust/pvimg/build.rs new file mode 100644 index 00000000..8381e92e --- /dev/null +++ b/rust/pvimg/build.rs @@ -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(()) +} diff --git a/rust/pvimg/src/cli.rs b/rust/pvimg/src/cli.rs new file mode 100644 index 00000000..929babd0 --- /dev/null +++ b/rust/pvimg/src/cli.rs @@ -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 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, + + /// 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, +} + +#[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, + + /// Disable Secure Execution guest dump support (default). + #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with="enable_dump", group="header-flags")] + pub disable_dump: Option, + + /// 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, + + /// 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, + + /// 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, + + /// 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, +} + +#[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, +} + +#[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, + + /// 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, +} + +/// 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, + + #[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 = ::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, + + #[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, + + /// 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, + + /// 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, + + /// 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, + + /// 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, + + /// 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, +} + +#[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), + + /// 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), + + /// 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, + } + + impl CliOption { + fn new, T: AsRef, 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 for Vec { + fn from(val: CliOption) -> Self { + let CliOption { args, .. } = val; + args + } + } + + fn flat_map_collect(map: BTreeMap) -> Vec { + map.into_values().flat_map(|v| v.args).collect() + } + + fn insert( + mut map: BTreeMap, + values: Vec, + ) -> BTreeMap { + for value in values { + map.insert(value.name.to_owned(), value); + } + map + } + + fn remove>( + mut map: BTreeMap, + key: S, + ) -> BTreeMap { + 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[=]' cannot be used with '--host-key-document + // ' + 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[=]' cannot be used with '--host-key-document + // ' + 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(); + } +} diff --git a/rust/pvimg/src/cmd.rs b/rust/pvimg/src/cmd.rs new file mode 100644 index 00000000..8fda1029 --- /dev/null +++ b/rust/pvimg/src/cmd.rs @@ -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; diff --git a/rust/pvimg/src/cmd/common.rs b/rust/pvimg/src/cmd/common.rs new file mode 100644 index 00000000..6b46ced3 --- /dev/null +++ b/rust/pvimg/src/cmd/common.rs @@ -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>)>, + pub(crate) components_key: Option<(PathBuf, Confidential>)>, + pub(crate) aead_key: Option<(PathBuf, Confidential>)>, +} + +/// Reads all user provided keys. +pub fn read_user_provided_keys( + cck_path: Option<&Path>, + experimental_args: &CreateBootImageExperimentalArgs, +) -> Result { + 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, + }) +} diff --git a/rust/pvimg/src/cmd/create.rs b/rust/pvimg/src/cmd/create.rs new file mode 100644 index 00000000..a22f244e --- /dev/null +++ b/rust/pvimg/src/cmd/create.rs @@ -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> { + // 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 = + 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> = [ + 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> = [ + 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 { + // 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 = 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) +} diff --git a/rust/pvimg/src/cmd/info.rs b/rust/pvimg/src/cmd/info.rs new file mode 100644 index 00000000..1ced0545 --- /dev/null +++ b/rust/pvimg/src/cmd/info.rs @@ -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 { + 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) +} diff --git a/rust/pvimg/src/cmd/test.rs b/rust/pvimg/src/cmd/test.rs new file mode 100644 index 00000000..806eecd1 --- /dev/null +++ b/rust/pvimg/src/cmd/test.rs @@ -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 { + 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

(hdr: &SeHdr, host_key_documents: &[P]) -> Result +where + P: AsRef, +{ + 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 { + 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 + }) +} diff --git a/rust/pvimg/src/cmd/version.rs b/rust/pvimg/src/cmd/version.rs new file mode 100644 index 00000000..5318d83b --- /dev/null +++ b/rust/pvimg/src/cmd/version.rs @@ -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 { + print_version!("2024", filter; FEATURES.concat()); + Ok(OwnExitCode::Success) +} diff --git a/rust/pvimg/src/main.rs b/rust/pvimg/src/main.rs new file mode 100644 index 00000000..38368be5 --- /dev/null +++ b/rust/pvimg/src/main.rs @@ -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), + } +} diff --git a/rust/pvimg/src/se_img.rs b/rust/pvimg/src/se_img.rs new file mode 100644 index 00000000..9f2b3dc6 --- /dev/null +++ b/rust/pvimg/src/se_img.rs @@ -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], + pub pcf: &'a PlaintextControlFlagsV1, + pub scf: &'a SecretControlFlagsV1, + pub cck: &'a Option<(PathBuf, Confidential>)>, + pub hdr_aead_key: &'a Option<(PathBuf, Confidential>)>, + pub psw_addr: &'a Option, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ImgComponent { + kind: ComponentKind, + pub(crate) src: Rc, + pub(crate) secure_mode: Option, +} + +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 { + 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 { + /// 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>, + builder: SecuredComponentBuilder, + stage3a: Vec, + stage3b: Vec, + /// The legacy Secure Execution header address (directly after stage3a) + legacy_se_hdr_addr: Option, + finalized: bool, +} + +impl SeImgBuilder { + 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, + bootloader_dir: Option<&PathBuf>, + ) -> Result { + 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( + &mut self, + component: &mut T, + tweak: Option>, + ) -> Result> + where + T: ComponentTrait, + { + 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>( + &mut self, + component: &mut T, + addr: u64, + ) -> Result> { + // 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>( + &mut self, + component: &mut T, + ) -> Result> { + 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> { + let img_comps_tweak_and_src: Result> = 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> { + 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>> { + 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> { + 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> { + 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> { + // 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>, + ) -> 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![]); + } +} diff --git a/rust/pvimg/src/se_img_comps.rs b/rust/pvimg/src/se_img_comps.rs new file mode 100644 index 00000000..dd04cd00 --- /dev/null +++ b/rust/pvimg/src/se_img_comps.rs @@ -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 { + /// 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 { + 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 { + 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 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 ReadSeekDebug for T {} + +#[derive(Debug)] +pub struct CompReader { + reader: Box, +} + +impl CompReader { + pub fn new(reader: Box) -> Self { + Self { reader } + } +} + +impl Read for CompReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.reader.read(buf) + } +} + +impl Seek for CompReader { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + 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 { + 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 { + 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); + } +} diff --git a/rust/pvimg/src/se_img_comps/bootloader.rs b/rust/pvimg/src/se_img_comps/bootloader.rs new file mode 100644 index 00000000..3afc8198 --- /dev/null +++ b/rust/pvimg/src/se_img_comps/bootloader.rs @@ -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, + stage3a_addr: u64, + se_hdr_src: &Interval, + ipib_src: &Interval, +) -> Result { + 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, + psw: PSW, + prepared_comps: &[Rc], +) -> Result { + 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::>>()?; + + 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)>, +) -> Result { + 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) +} diff --git a/rust/pvimg/src/se_img_comps/bootloader/ipl.rs b/rust/pvimg/src/se_img_comps/bootloader/ipl.rs new file mode 100644 index 00000000..c646612c --- /dev/null +++ b/rust/pvimg/src/se_img_comps/bootloader/ipl.rs @@ -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, +} + +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 { + 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 { + 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) + } +} diff --git a/rust/pvimg/src/se_img_comps/bootloader/stage3a_defs.rs b/rust/pvimg/src/se_img_comps/bootloader/stage3a_defs.rs new file mode 100644 index 00000000..0e866e2a --- /dev/null +++ b/rust/pvimg/src/se_img_comps/bootloader/stage3a_defs.rs @@ -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 = ::std::mem::MaybeUninit::uninit(); + let ptr = UNINIT.as_ptr(); + assert_eq!( + ::std::mem::size_of::(), + 24_usize, + concat!("Size of: ", stringify!(stage3a_args)) + ); + assert_eq!( + ::std::mem::align_of::(), + 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) + ) + ); +} diff --git a/rust/pvimg/src/se_img_comps/bootloader/stage3b_defs.rs b/rust/pvimg/src/se_img_comps/bootloader/stage3b_defs.rs new file mode 100644 index 00000000..d852f56f --- /dev/null +++ b/rust/pvimg/src/se_img_comps/bootloader/stage3b_defs.rs @@ -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 = ::std::mem::MaybeUninit::uninit(); + let ptr = UNINIT.as_ptr(); + assert_eq!( + ::std::mem::size_of::(), + 16_usize, + concat!("Size of: ", stringify!(memblob)) + ); + assert_eq!( + ::std::mem::align_of::(), + 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 = ::std::mem::MaybeUninit::uninit(); + let ptr = UNINIT.as_ptr(); + assert_eq!( + ::std::mem::size_of::(), + 64_usize, + concat!("Size of: ", stringify!(stage3b_args)) + ); + assert_eq!( + ::std::mem::align_of::(), + 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) + ) + ); +} diff --git a/rust/pvimg/src/se_img_comps/cmdline.rs b/rust/pvimg/src/se_img_comps/cmdline.rs new file mode 100644 index 00000000..4fb29d4e --- /dev/null +++ b/rust/pvimg/src/se_img_comps/cmdline.rs @@ -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, +} + +impl Cmdline { + pub fn new(reader: Box) -> Self { + Self { + comp: CompReader { reader }, + last_value: None, + } + } +} + +impl Read for Cmdline { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + // 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 { + // 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 for Cmdline { + fn kind(&self) -> ComponentKind { + ComponentKind::Cmdline + } + + fn secure_mode(&self) -> bool { + true + } +} diff --git a/rust/pvimg/src/se_img_comps/ipib.rs b/rust/pvimg/src/se_img_comps/ipib.rs new file mode 100644 index 00000000..01d0b55e --- /dev/null +++ b/rust/pvimg/src/se_img_comps/ipib.rs @@ -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) -> Self { + Self(CompReader { reader }) + } +} + +impl Read for Ipib { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.0.read(buf) + } +} + +impl Seek for Ipib { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + 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 for Ipib { + fn secure_mode(&self) -> bool { + false + } + + fn kind(&self) -> ComponentKind { + ComponentKind::Ipib + } +} diff --git a/rust/pvimg/src/se_img_comps/kernel.rs b/rust/pvimg/src/se_img_comps/kernel.rs new file mode 100644 index 00000000..a45cd6e5 --- /dev/null +++ b/rust/pvimg/src/se_img_comps/kernel.rs @@ -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) -> Self { + Self(CompReader { reader }) + } + + fn is_elf_file(&mut self) -> Result { + 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 { + 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 { + 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 { + self.0.read(buf) + } +} + +impl Seek for S390Kernel { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + 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 for S390Kernel { + fn secure_mode(&self) -> bool { + true + } + + fn kind(&self) -> ComponentKind { + ComponentKind::Kernel + } +} diff --git a/rust/pvimg/src/se_img_comps/ramdisk.rs b/rust/pvimg/src/se_img_comps/ramdisk.rs new file mode 100644 index 00000000..01964ae7 --- /dev/null +++ b/rust/pvimg/src/se_img_comps/ramdisk.rs @@ -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) -> Self { + Self(CompReader { reader }) + } +} + +impl Read for Ramdisk { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.0.read(buf) + } +} + +impl Seek for Ramdisk { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + 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 for Ramdisk { + fn kind(&self) -> ComponentKind { + ComponentKind::Ramdisk + } + + fn secure_mode(&self) -> bool { + true + } +} diff --git a/rust/pvimg/src/se_img_comps/sehdr.rs b/rust/pvimg/src/se_img_comps/sehdr.rs new file mode 100644 index 00000000..1399a9c0 --- /dev/null +++ b/rust/pvimg/src/se_img_comps/sehdr.rs @@ -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) -> Self { + Self(CompReader { reader }) + } +} + +impl Seek for SeHdrComp { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.0.seek(pos) + } +} + +impl Read for SeHdrComp { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + 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 for SeHdrComp { + fn kind(&self) -> ComponentKind { + ComponentKind::SeHdr + } + + fn secure_mode(&self) -> bool { + false + } +} diff --git a/rust/pvimg/src/se_img_comps/shortpsw.rs b/rust/pvimg/src/se_img_comps/shortpsw.rs new file mode 100644 index 00000000..c311357d --- /dev/null +++ b/rust/pvimg/src/se_img_comps/shortpsw.rs @@ -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) -> Self { + Self(CompReader { reader }) + } +} + +impl Read for ShortPSWComp { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.0.read(buf) + } +} + +impl Seek for ShortPSWComp { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + 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 for ShortPSWComp { + fn kind(&self) -> ComponentKind { + ComponentKind::ShortPSW + } + + fn secure_mode(&self) -> bool { + false + } +} diff --git a/rust/pvimg/src/se_img_comps/stage3a.rs b/rust/pvimg/src/se_img_comps/stage3a.rs new file mode 100644 index 00000000..d820ce82 --- /dev/null +++ b/rust/pvimg/src/se_img_comps/stage3a.rs @@ -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) -> Self { + Self(CompReader { reader }) + } +} + +impl Read for Stage3a { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.0.read(buf) + } +} +impl Seek for Stage3a { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + 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 for Stage3a { + fn kind(&self) -> ComponentKind { + ComponentKind::Stage3a + } + + fn secure_mode(&self) -> bool { + false + } +} diff --git a/rust/pvimg/src/se_img_comps/stage3b.rs b/rust/pvimg/src/se_img_comps/stage3b.rs new file mode 100644 index 00000000..028419f8 --- /dev/null +++ b/rust/pvimg/src/se_img_comps/stage3b.rs @@ -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) -> Self { + Self(CompReader::new(reader)) + } +} + +impl Read for Stage3b { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.0.read(buf) + } +} + +impl Seek for Stage3b { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + 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 for Stage3b { + fn kind(&self) -> ComponentKind { + ComponentKind::Stage3b + } + + fn secure_mode(&self) -> bool { + true + } +} diff --git a/rust/utils/src/cli.rs b/rust/utils/src/cli.rs index fccbba75..066e939f 100644 --- a/rust/utils/src/cli.rs +++ b/rust/utils/src/cli.rs @@ -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"])), )]