From ce8201082759004c845a43ff3d58b60f77e2017d Mon Sep 17 00:00:00 2001 From: Annu Sharma Date: Wed, 3 Dec 2025 16:31:04 +0000 Subject: [PATCH] Add tool to display Secure Execution system information Add 'pvinfo' a tool to display the information of enabled flags, and print the debug information in Yaml and Human format. Reviewed-by: Steffen Eiden Signed-off-by: Serena Santosh Signed-off-by: Ann Mariya Jojo Signed-off-by: Annu Sharma Signed-off-by: Steffen Eiden --- rust/Cargo.lock | 14 + rust/Cargo.toml | 1 + rust/Makefile | 7 +- rust/pvinfo/Cargo.toml | 25 + rust/pvinfo/build.rs | 26 + rust/pvinfo/src/cli.rs | 230 +++++++++ rust/pvinfo/src/constants.rs | 43 ++ rust/pvinfo/src/handlers.rs | 190 ++++++++ rust/pvinfo/src/io_utils.rs | 320 +++++++++++++ rust/pvinfo/src/main.rs | 58 +++ rust/pvinfo/src/pvinfo.rs | 447 ++++++++++++++++++ rust/pvinfo/src/se_status.rs | 99 ++++ rust/pvinfo/src/strings.rs | 145 ++++++ rust/pvinfo/tests/assets/prot_virt_guest | 1 + rust/pvinfo/tests/assets/prot_virt_host | 1 + .../tests/assets/query/dump_finalize_len | 1 + .../tests/assets/query/dump_storage_state_len | 1 + rust/pvinfo/tests/assets/query/facilities | 4 + .../tests/assets/query/feature_indications | 1 + rust/pvinfo/tests/assets/query/max_address | 1 + .../tests/assets/query/max_assoc_secrets | 1 + rust/pvinfo/tests/assets/query/max_cpus | 1 + rust/pvinfo/tests/assets/query/max_guests | 1 + .../tests/assets/query/max_retr_secrets | 1 + rust/pvinfo/tests/assets/query/max_secrets | 1 + .../tests/assets/query/supp_add_secret_pcf | 1 + .../assets/query/supp_add_secret_req_ver | 1 + .../pvinfo/tests/assets/query/supp_att_pflags | 1 + .../tests/assets/query/supp_att_req_hdr_ver | 1 + .../pvinfo/tests/assets/query/supp_se_hdr_pcf | 1 + .../pvinfo/tests/assets/query/supp_se_hdr_ver | 1 + .../tests/assets/query/supp_secret_types | 1 + .../tests/assets/query/uv_query_dump_cpu_len | 1 + 33 files changed, 1626 insertions(+), 2 deletions(-) create mode 100644 rust/pvinfo/Cargo.toml create mode 100644 rust/pvinfo/build.rs create mode 100644 rust/pvinfo/src/cli.rs create mode 100644 rust/pvinfo/src/constants.rs create mode 100644 rust/pvinfo/src/handlers.rs create mode 100644 rust/pvinfo/src/io_utils.rs create mode 100644 rust/pvinfo/src/main.rs create mode 100644 rust/pvinfo/src/pvinfo.rs create mode 100644 rust/pvinfo/src/se_status.rs create mode 100644 rust/pvinfo/src/strings.rs create mode 100644 rust/pvinfo/tests/assets/prot_virt_guest create mode 100644 rust/pvinfo/tests/assets/prot_virt_host create mode 100644 rust/pvinfo/tests/assets/query/dump_finalize_len create mode 100644 rust/pvinfo/tests/assets/query/dump_storage_state_len create mode 100644 rust/pvinfo/tests/assets/query/facilities create mode 100644 rust/pvinfo/tests/assets/query/feature_indications create mode 100644 rust/pvinfo/tests/assets/query/max_address create mode 100644 rust/pvinfo/tests/assets/query/max_assoc_secrets create mode 100644 rust/pvinfo/tests/assets/query/max_cpus create mode 100644 rust/pvinfo/tests/assets/query/max_guests create mode 100644 rust/pvinfo/tests/assets/query/max_retr_secrets create mode 100644 rust/pvinfo/tests/assets/query/max_secrets create mode 100644 rust/pvinfo/tests/assets/query/supp_add_secret_pcf create mode 100644 rust/pvinfo/tests/assets/query/supp_add_secret_req_ver create mode 100644 rust/pvinfo/tests/assets/query/supp_att_pflags create mode 100644 rust/pvinfo/tests/assets/query/supp_att_req_hdr_ver create mode 100644 rust/pvinfo/tests/assets/query/supp_se_hdr_pcf create mode 100644 rust/pvinfo/tests/assets/query/supp_se_hdr_ver create mode 100644 rust/pvinfo/tests/assets/query/supp_secret_types create mode 100644 rust/pvinfo/tests/assets/query/uv_query_dump_cpu_len diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 6ce41409..ddc4162b 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -652,6 +652,20 @@ dependencies = [ "utils", ] +[[package]] +name = "pvinfo" +version = "0.12.0" +dependencies = [ + "anyhow", + "clap", + "clap_complete", + "s390_pv_core", + "serde", + "serde_yaml", + "tempfile", + "utils", +] + [[package]] name = "pvsecret" version = "0.12.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b2273cf5..b9d8e5ad 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -6,6 +6,7 @@ members = [ "pvapconfig", "pvattest", "pvimg", + "pvinfo", "pvsecret", "utils", ] diff --git a/rust/Makefile b/rust/Makefile index 107dd2be..f93f1983 100644 --- a/rust/Makefile +++ b/rust/Makefile @@ -28,9 +28,9 @@ ifneq (${HAVE_LIBCURL},0) PV_TARGETS := pvsecret pvattest pvimg ifeq ($(HOST_ARCH),s390x) - PV_TARGETS += pvapconfig + PV_TARGETS += pvapconfig pvinfo else - BUILD_TARGETS += skip-pvapconfig + BUILD_TARGETS += skip-pvapconfig skip-pvinfo endif #HOSTARCH PV_BUILD_TARGETS := $(PV_TARGETS) @@ -75,6 +75,9 @@ skip-build: skip-pv-build: echo " SKIP rust-pv-tools due to unresolved dependencies" +skip-pvinfo: + echo " SKIP pvinfo due to unsupported architecture (s390x only)" + skip-pvapconfig: echo " SKIP pvapconfig due to unsupported architecture (s390x only)" diff --git a/rust/pvinfo/Cargo.toml b/rust/pvinfo/Cargo.toml new file mode 100644 index 00000000..74e60b67 --- /dev/null +++ b/rust/pvinfo/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "pvinfo" +version = "0.12.0" +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +anyhow = "1" +clap = { version = "4.5", features = ["derive"] } +serde = { version = "1.0.217", features = ["derive"] } +serde_yaml = "0.9.21" + +pv_core = { path = "../pv_core", package = "s390_pv_core" } +utils = { path = "../utils" } + +[dev-dependencies] +tempfile = "3.15.0" + +[build-dependencies] +clap = { version = "4.5", features = ["derive"] } +clap_complete = "4.5" diff --git a/rust/pvinfo/build.rs b/rust/pvinfo/build.rs new file mode 100644 index 00000000..91212adf --- /dev/null +++ b/rust/pvinfo/build.rs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 +// it under the terms of the MIT license. See LICENSE for details. +#![allow(missing_docs)] + +use clap::CommandFactory; +use clap_complete::{generate_to, Shell}; +use std::env; +use std::io::Error; + +include!("src/cli.rs"); + +fn main() -> Result<(), Error> { + let outdir = env::var_os("OUT_DIR").unwrap(); + let crate_name = env!("CARGO_PKG_NAME"); + let mut cmd = CliOptions::command(); + for &shell in Shell::value_variants() { + generate_to(shell, &mut cmd, crate_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/pvinfo/src/cli.rs b/rust/pvinfo/src/cli.rs new file mode 100644 index 00000000..27b0eb46 --- /dev/null +++ b/rust/pvinfo/src/cli.rs @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2025 +use clap::{Parser, Subcommand, ValueEnum}; + +/// Output format for pvinfo results +#[derive(Copy, Clone, Debug, ValueEnum, Default)] +pub enum OutputFormat { + #[default] + + /// Human-readable format + Human, + + /// YAML format + Yaml, +} + +/// Query information about IBM Secure Execution system status +/// +/// The PV Info Tool queries and displays information provided by the +/// Ultravisor about IBM Secure Execution host and guest systems. +/// It can show the Secure Execution (SE) status, installed facilities, +/// supported features, supported flags, and limits. +/// +/// By default, running `pvinfo` without arguments prints all available +/// information in human-readable format. +/// Specific flags allow focused queries (e.g., only SE status, only limits). +/// Output can also be formatted as YAML. +#[derive(Parser, Debug)] +#[command(author)] +pub struct CliOptions { + /// Display the Secure Execution status of the system + /// + /// Prints whether this system is running as an SE-host, + /// SE-guest, or none. + #[arg(long)] + pub se_status: bool, + + /// Show installed Ultravisor calls + /// + /// Lists all facilities supported by the Ultravisor. + #[arg(long)] + pub facilities: bool, + + /// Show Ultravisor feature indications + /// + /// Lists the feature bits reported by the Ultravisor that describe + /// available Secure Execution functionality. + #[arg(long)] + pub feature_indications: bool, + + /// Show supported plaintext attestation flags + /// + /// Prints the list of flags that can be used in attestation requests. + #[arg(long)] + pub supported_plaintext_attestation_flags: bool, + + /// Show supported SE header versions + /// + /// Prints the list of header versions supported. + #[arg(long)] + pub supported_se_header_versions: bool, + + /// Show supported secret types + /// + /// Lists the types of secrets. + #[arg(long)] + pub supported_secret_types: bool, + + /// Show supported plaintext control flags + /// + /// Lists control flags that can be set . + #[arg(long)] + pub supported_plaintext_control_flags: bool, + + /// Show supported attestation request versions + /// + /// Lists the versions of attestation request . + #[arg(long)] + pub supported_attestation_request_versions: bool, + + /// Show supported add-secret request versions + /// + /// Lists the versions of add-secret request supported. + #[arg(long)] + pub supported_add_secret_request_versions: bool, + + /// Show supported plaintext add-secret flags + /// + /// Lists control flags available for plaintext Add-secret requests. + #[arg(long)] + pub supported_plaintext_add_secret_flags: bool, + + /// Show Secure Execution limits + /// + /// Prints limits such as the maximum number of CPUs, + /// maximum guests, maximum retrievable secrets, etc. + #[arg(long)] + pub limits: bool, + + /// Print version information and exit + #[arg(long)] + pub version: bool, + + /// Select the output format + /// + /// By default, output is human-readable text. + /// Use `--format yaml` to produce YAML output instead. + #[arg(long, value_enum, default_value_t)] + pub format: OutputFormat, + + #[command(subcommand)] + pub command: Option, +} + +/// Additional commands to query supported flags +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Query supported flags grouped by category + /// + /// Provides detailed information about supported secret types, + /// attestation flags, and header versions. + /// By default, all categories are shown if no specific options are set. + SupportedFlags { + /// Show supported secret-related flags + #[arg(long)] + secret: bool, + + /// Show supported attestation-related flags + #[arg(long)] + attestation: bool, + + /// Show supported header-related flags + #[arg(long)] + header: bool, + }, +} + +impl CliOptions { + /// Returns true if any query flags or subcommands were provided. + pub fn any_flags_set(&self) -> bool { + self.se_status + || self.facilities + || self.feature_indications + || self.supported_plaintext_attestation_flags + || self.supported_se_header_versions + || self.supported_secret_types + || self.supported_plaintext_control_flags + || self.supported_attestation_request_versions + || self.supported_add_secret_request_versions + || self.supported_plaintext_add_secret_flags + || self.limits + || self.command.is_some() + } + + /// Sets all flags to true if no flags or subcommands are set. + pub fn post_process(&mut self) { + if !self.any_flags_set() { + self.se_status = true; + self.facilities = true; + self.feature_indications = true; + self.supported_plaintext_attestation_flags = true; + self.supported_se_header_versions = true; + self.supported_secret_types = true; + self.supported_plaintext_control_flags = true; + self.supported_attestation_request_versions = true; + self.supported_add_secret_request_versions = true; + self.supported_plaintext_add_secret_flags = true; + self.limits = true; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Helper function to create a base CLI with all flags false + fn base_cli() -> CliOptions { + CliOptions { + se_status: false, + facilities: false, + feature_indications: false, + supported_plaintext_attestation_flags: false, + supported_se_header_versions: false, + supported_secret_types: false, + supported_plaintext_control_flags: false, + supported_attestation_request_versions: false, + supported_add_secret_request_versions: false, + supported_plaintext_add_secret_flags: false, + limits: false, + version: false, + command: None, + format: OutputFormat::Human, + } + } + + #[test] + fn test_any_flags_set_false_when_all_false() { + let cli = base_cli(); + assert!(!cli.any_flags_set()); + } + + #[test] + fn test_any_flags_set_true_when_single_flag_true() { + let mut cli = base_cli(); + cli.se_status = true; + assert!(cli.any_flags_set()); + } + + #[test] + fn test_any_flags_set_true_when_command_set() { + let mut cli = base_cli(); + cli.command = Some(Commands::SupportedFlags { + secret: true, + attestation: true, + header: true, + }); + assert!(cli.any_flags_set()); + } + + #[test] + fn test_any_flags_set_true_when_multiple_flags_true() { + let mut cli = base_cli(); + cli.se_status = true; + cli.limits = true; + cli.feature_indications = true; + assert!(cli.any_flags_set()); + } +} diff --git a/rust/pvinfo/src/constants.rs b/rust/pvinfo/src/constants.rs new file mode 100644 index 00000000..4c6742c4 --- /dev/null +++ b/rust/pvinfo/src/constants.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2025 + +//! Constants used throughout the PV Info Tool + +// Base directories +#[cfg(test)] +pub const BASE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/assets"); + +#[cfg(not(test))] +pub const BASE_DIR: &str = "/sys/firmware/uv"; + +pub const QUERY_DIR: &str = "query"; + +// File names +pub const FACILITIES: &str = "facilities"; +pub const FEATURE_INDICATIONS: &str = "feature_indications"; +pub const PROT_VIRT_GUEST: &str = "prot_virt_guest"; +pub const PROT_VIRT_HOST: &str = "prot_virt_host"; +pub const MAX_ADDRESS: &str = "max_address"; +pub const MAX_ASSOC_SECRETS: &str = "max_assoc_secrets"; +pub const MAX_CPUS: &str = "max_cpus"; +pub const MAX_GUESTS: &str = "max_guests"; +pub const MAX_RETR_SECRETS: &str = "max_retr_secrets"; +pub const MAX_SECRETS: &str = "max_secrets"; +pub const SUPP_ADD_SECRET_PCF: &str = "supp_add_secret_pcf"; +pub const SUPP_ADD_SECRET_REQ_VER: &str = "supp_add_secret_req_ver"; +pub const SUPP_ATT_PFLAGS: &str = "supp_att_pflags"; +pub const SUPP_ATT_REQ_HDR_VER: &str = "supp_att_req_hdr_ver"; +pub const SUPP_SE_HDR_PCF: &str = "supp_se_hdr_pcf"; +pub const SUPP_SE_HDR_VER: &str = "supp_se_hdr_ver"; +pub const SUPP_SECRET_TYPES: &str = "supp_secret_types"; + +// Limits +pub const LIMITS: [(&str, &str); 6] = [ + (MAX_ADDRESS, "Maximal Address for a SE-Guest"), + (MAX_ASSOC_SECRETS, "Maximal number of associated secrets"), + (MAX_CPUS, "Maximal number of CPUs in one SE-Guest"), + (MAX_GUESTS, "Maximal number of SE-Guests"), + (MAX_RETR_SECRETS, "Maximal number of retrievable secrets"), + (MAX_SECRETS, "Maximal number of secrets in the system"), +]; diff --git a/rust/pvinfo/src/handlers.rs b/rust/pvinfo/src/handlers.rs new file mode 100644 index 00000000..6758d7dc --- /dev/null +++ b/rust/pvinfo/src/handlers.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2025 + +//! Handlers for processing CLI flags and subcommands + +use crate::constants::*; +use crate::io_utils::{collect_bit_messages, collect_version_flags, read_hex_from_file}; +use crate::strings::*; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; + +// Represents metadata for a supported content file. +struct Content { + file: &'static str, + label: &'static str, + desc_content: Option<&'static str>, +} + +impl Content { + //Reads a hex file, then prints either: + // bit messages or version flags + fn handle_flag(&self, writer: &mut dyn Write, query_dir: &Path) -> io::Result { + let hex = match read_hex_from_file(query_dir.join(self.file), self.label) { + Some(h) => h, + None => { + return Ok(String::new()); + } + }; + + let mut output = String::new(); + let header = format!("\n{}:", self.label); + writeln!(writer, "{header}")?; + output.push_str(&header); + + match self.desc_content { + Some(desc) => { + for msg in collect_bit_messages(hex, desc) { + writeln!(writer, "{msg}")?; + output.push_str(&format!("\n{msg}")); + } + } + None => { + for v in collect_version_flags(hex) { + writeln!(writer, "{v}")?; + output.push_str(&format!("\n{v}")); + } + } + } + + Ok(output) + } +} + +// Define constants for each supported flag group +const SUPP_SECRET_C: Content = Content { + file: SUPP_SECRET_TYPES, + label: "Supported Secret types", + desc_content: Some(SUPP_SECRET_TYPES_DESC), +}; + +const SUPP_ADD_SECRET_REQ_C: Content = Content { + file: SUPP_ADD_SECRET_REQ_VER, + label: "Supported Add Secret Request Versions", + desc_content: None, +}; + +const SUPP_ADD_SECRET_PCF_C: Content = Content { + file: SUPP_ADD_SECRET_PCF, + label: "Supported Plaintext Add Secret Flags", + desc_content: Some(SUPP_ADD_SECRET_PCF_DESC), +}; + +const SUPP_ATT_PFLAGS_C: Content = Content { + file: SUPP_ATT_PFLAGS, + label: "Supported Plaintext Attestation Flags", + desc_content: Some(SUPP_ATT_PFLAGS_DESC), +}; + +const SUPP_ATT_REQ_HDR_VER_C: Content = Content { + file: SUPP_ATT_REQ_HDR_VER, + label: "Supported Attestation Request Versions", + desc_content: None, +}; + +const SUPP_SE_HDR_VER_C: Content = Content { + file: SUPP_SE_HDR_VER, + label: "Supported SE Header Versions", + desc_content: None, +}; + +const SUPP_SE_HDR_PCF_C: Content = Content { + file: SUPP_SE_HDR_PCF, + label: "Supported Plaintext Control Flags", + desc_content: Some(SUPP_SE_HDR_PCF_DESC), +}; + +// Main entry point for handling the supported-flags subcommand. +pub fn handle_supported_flags( + writer: &mut dyn Write, + secret: bool, + attestation: bool, + header: bool, + query_dir: PathBuf, +) -> io::Result> { + let mut results = Vec::new(); + + if secret { + results.push(SUPP_SECRET_C.handle_flag(writer, &query_dir)?); + results.push(SUPP_ADD_SECRET_REQ_C.handle_flag(writer, &query_dir)?); + results.push(SUPP_ADD_SECRET_PCF_C.handle_flag(writer, &query_dir)?); + } + + if attestation { + results.push(SUPP_ATT_PFLAGS_C.handle_flag(writer, &query_dir)?); + results.push(SUPP_ATT_REQ_HDR_VER_C.handle_flag(writer, &query_dir)?); + } + + if header { + results.push(SUPP_SE_HDR_VER_C.handle_flag(writer, &query_dir)?); + results.push(SUPP_SE_HDR_PCF_C.handle_flag(writer, &query_dir)?); + } + + if results.is_empty() { + let mut all_results = handle_supported_flags(writer, true, true, true, query_dir)?; + results.append(&mut all_results); + } + + Ok(results) +} + +// Unit Tests for handlers +#[cfg(test)] +mod test { + use super::*; + use std::fs; + use tempfile::tempdir; + + // Verifies that handle_supported_flags correctly processes all + #[test] + fn test_handle_supported_flags_with_buffer() { + let dir = tempdir().unwrap(); + + // Creating files + fs::write(dir.path().join(SUPP_SECRET_TYPES), "1").unwrap(); + fs::write(dir.path().join(SUPP_ADD_SECRET_REQ_VER), "1").unwrap(); + fs::write(dir.path().join(SUPP_ADD_SECRET_PCF), "1").unwrap(); + fs::write(dir.path().join(SUPP_ATT_PFLAGS), "1").unwrap(); + fs::write(dir.path().join(SUPP_ATT_REQ_HDR_VER), "1").unwrap(); + fs::write(dir.path().join(SUPP_SE_HDR_VER), "1").unwrap(); + fs::write(dir.path().join(SUPP_SE_HDR_PCF), "1").unwrap(); + + // Capture output into buffer + let mut buffer = Vec::new(); + let results = + handle_supported_flags(&mut buffer, true, true, true, dir.path().to_path_buf()) + .unwrap(); + + // Convert buffer to string + let printed = String::from_utf8(buffer).unwrap(); + + let normalized: String = printed + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .collect::>() + .join(" "); + + let expected = [ + "Supported Secret types:", + "Supported Add Secret Request Versions:", + "Supported Plaintext Add Secret Flags:", + "Supported Plaintext Attestation Flags:", + "Supported Attestation Request Versions:", + "Supported SE Header Versions:", + "Supported Plaintext Control Flags:", + ]; + + for label in &expected { + assert!( + normalized.contains(label), + "{label} missing in printed output!\nNormalized buffer:\n{normalized}" + ); + assert!( + results.iter().any(|r| r.contains(label)), + "{label} missing in results Vec!" + ); + } + } +} diff --git a/rust/pvinfo/src/io_utils.rs b/rust/pvinfo/src/io_utils.rs new file mode 100644 index 00000000..74f35322 --- /dev/null +++ b/rust/pvinfo/src/io_utils.rs @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2025 + +//! I/O utilities for the PV Info Tool + +use crate::constants::*; +use anyhow::{Context, Result}; +use pv_core::misc::{read_file, read_file_string, try_parse_u64}; +use pv_core::misc::{Flags, Msb0Flags64}; +use std::collections::HashMap; +use std::path::Path; +use std::str; + +// Verify that the Ultravisor directory exists +pub fn check_uv_exists() -> Result<()> { + let uv_path = Path::new(BASE_DIR); + if !uv_path.exists() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Ultravisor directory missing", + )) + .context("This system is neither a SE-host or SE-guest"); + } + Ok(()) +} + +// File readers +pub fn read_hex_from_file>(path: P, context: &str) -> Option { + // Add precise context for which file failed + let content_str = read_file_string(path.as_ref(), context).ok()?; + let first_line = content_str.lines().next()?.trim(); + if first_line.is_empty() { + return None; + } + + match try_parse_u64(first_line, context) { + Ok(val) if val != 0 => Some(val), + Ok(_) => None, // treat "0x0" as absent + Err(e) => panic!( + "Failed to parse hex value in {} ({}): {}", + path.as_ref().display(), + context, + e + ), + } +} + +pub fn read_integer_from_file>(path: P, context: &str) -> Option { + let content_str = read_file_string(path.as_ref(), context).ok()?; + let first_line = content_str.lines().next()?.trim(); + if first_line.is_empty() { + return None; + } + match first_line.parse::() { + Ok(val) => Some(val), + Err(e) => panic!( + "Failed to parse integer in {} ({}): {}", + path.as_ref().display(), + context, + e + ), + } +} + +pub fn read_bool_from_file>(path: P, context: &str) -> bool { + let content = read_file(path.as_ref(), context).unwrap_or_else(|e| { + panic!( + "Failed to read {} ({}): {}", + path.as_ref().display(), + context, + e + ) + }); + + let content_str = str::from_utf8(&content) + .unwrap_or_else(|_| panic!("Invalid UTF-8 in {} ({})", path.as_ref().display(), context)); + + match content_str.lines().next().unwrap_or("").trim() { + "1" => true, + "0" => false, + other => panic!( + "Invalid boolean value in {} ({}): {}", + path.as_ref().display(), + context, + other + ), + } +} + +// Collectors +pub fn collect_bit_messages(hex_value: u64, desc_content: &str) -> Vec { + let flags = Msb0Flags64::from(hex_value); + let mut messages = Vec::new(); + + for (line_index, line) in desc_content.lines().enumerate() { + if flags.is_set(line_index as u8) { + if line.to_lowercase().contains("reserved") { + messages.push(format!("{line} Bit-{line_index}")); + } else { + messages.push(line.to_string()); + } + } + } + + if messages.is_empty() { + messages.push("No matching messages. But these bits are ON:".into()); + for bit in 0..64 { + if flags.is_set(bit) { + messages.push(format!("- Bit {bit} is ON")); + } + } + } + + messages +} + +pub fn collect_version_flags(hex_value: u64) -> Vec { + let flags = Msb0Flags64::from(hex_value); + let mut versions = Vec::new(); + + for bit in 0..64 { + if flags.is_set(bit as u8) { + let version = (bit + 1) * 0x100; + versions.push(format!("version {version:x} hex is supported")); + } + } + + versions +} + +pub fn collect_limits(query_dir: &Path) -> HashMap { + let mut map = HashMap::new(); + for (file, desc) in LIMITS { + if let Some(val) = read_integer_from_file(query_dir.join(file), file) { + map.insert(desc.to_string(), val); + } + } + map +} + +#[cfg(test)] +mod test { + //! Unit Tests for io_utils + + use super::*; + use std::io::{self, Write}; + use std::path::Path; + use tempfile::{tempdir, NamedTempFile}; + + // Tests check_uv_exists() by observing the real BASE_DIR at test runtime + // if BASE_DIR exists on the machine running the tests, check_uv_exists() must return Ok + // otherwise, it must return Err(anyhow::Error) whose cause is an io::ErrorKind::NotFound, + // and whose message also contains the added context string + + #[test] + fn test_check_uv_exists_behaviour_matches_base_dir() { + let base_dir = BASE_DIR; + let uv_path = Path::new(base_dir); + let res = check_uv_exists(); + + if uv_path.exists() { + assert!( + res.is_ok(), + "BASE_DIR exists but check_uv_exists returned Err: {res:?}" + ); + } else { + assert!( + res.is_err(), + "BASE_DIR missing but check_uv_exists returned Ok" + ); + if let Err(e) = res { + // downcast anyhow::Error into std::io::Error + if let Some(io_err) = e.downcast_ref::() { + assert_eq!(io_err.kind(), io::ErrorKind::NotFound); + } else { + panic!("Expected std::io::Error inside anyhow::Error, got: {e:?}"); + } + + // ensure both the base error and context message appear + let msg = format!("{e}"); + assert!(msg.contains("Ultravisor directory missing")); + assert!(msg.contains("This system is neither a SE-host or SE-guest")); + } + } + } + + // read_hex_from_file tests + + #[test] + fn test_read_hex_from_file_valid() { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "0x1a2b").unwrap(); + let got = read_hex_from_file(file.path(), "test"); + assert_eq!(got, Some(0x1a2b)); + } + + #[test] + fn test_read_hex_from_file_zero_is_none() { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "0x0").unwrap(); + assert_eq!(read_hex_from_file(file.path(), "test"), None); + } + + #[test] + #[should_panic(expected = "Failed to parse hex value")] + fn test_read_hex_from_file_invalid_panics() { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "not_hex").unwrap(); + // should panic now + let _ = read_hex_from_file(file.path(), "test"); + } + + #[test] + fn test_read_hex_from_file_missing_returns_none() { + // missing file -> None + let dir = tempdir().unwrap(); + let missing = dir.path().join("no_such_file"); + assert_eq!(read_hex_from_file(missing, "test"), None); + } + + // read_integer_from_file tests + + #[test] + fn test_read_integer_from_file_valid() { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "12345").unwrap(); + assert_eq!(read_integer_from_file(file.path(), "test"), Some(12345)); + } + + #[test] + fn test_read_integer_from_file_empty_returns_none() { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file).unwrap(); + assert_eq!(read_integer_from_file(file.path(), "test"), None); + } + + #[test] + #[should_panic(expected = "Failed to parse integer")] + fn test_read_integer_from_file_invalid_panics() { + let mut file2 = NamedTempFile::new().unwrap(); + writeln!(file2, "abc").unwrap(); + // should panic now + let _ = read_integer_from_file(file2.path(), "test"); + } + + // read_bool_from_file tests + + #[test] + fn test_read_bool_from_file_true() { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "1").unwrap(); + assert!(read_bool_from_file(file.path(), "test")); + } + + #[test] + fn test_read_bool_from_file_false() { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "0").unwrap(); + assert!(!read_bool_from_file(file.path(), "test")); + } + + #[test] + #[should_panic(expected = "Invalid boolean value")] + fn test_read_bool_from_file_invalid_value_panics() { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "yes").unwrap(); + // should panic with clear error + let _ = read_bool_from_file(file.path(), "test"); + } + + // collect_bit_messages tests + + #[test] + fn test_collect_bit_messages_matches_and_reserved() { + // lines: index 0 -> bit 63, index 1 -> bit 62, index 2 -> bit 61 + let desc = "First Feature\nreserved for future use\nThird Feature"; + + // set bit for line 0 and line 2 + let hex = (1u64 << 63) | (1u64 << 61); + let messages = collect_bit_messages(hex, desc); + + // Expect "First Feature" and "Third Feature" (and the reserved line should get the " Bit-" if matched) + assert!(messages.iter().any(|m| m == "First Feature")); + assert!(messages.iter().any(|m| m == "Third Feature")); + // reserved wasn't set here; now test reserved specifically below + } + + #[test] + fn test_collect_bit_messages_reserved_line() { + let desc = "one\nReserved entry\nthree"; + // set the bit corresponding to line index 1 -> bit 62 + let hex = 1u64 << (63 - 1); + let messages = collect_bit_messages(hex, desc); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0], "Reserved entry Bit-1"); + } + + #[test] + fn test_collect_bit_messages_fallback_lists_bits() { + let desc = "only one line"; + // pick a bit that does not map to line 0 (63) so fallback triggers; e.g. bit 5 + let hex = 1u64 << 5; + let messages = collect_bit_messages(hex, desc); + assert!(!messages.is_empty()); + assert!(messages[0].starts_with("No matching messages")); + assert!(messages.iter().any(|m| m.contains("Bit 5"))); + } + + // collect_version_flags tests + + #[test] + fn test_collect_version_flags_two_bits() { + // top two bits set => versions 0x100 and 0x200 should be present + let hex = (1u64 << 63) | (1u64 << 62); + let versions = collect_version_flags(hex); + assert!(versions.contains(&"version 100 hex is supported".to_string())); + assert!(versions.contains(&"version 200 hex is supported".to_string())); + } +} diff --git a/rust/pvinfo/src/main.rs b/rust/pvinfo/src/main.rs new file mode 100644 index 00000000..22750aa1 --- /dev/null +++ b/rust/pvinfo/src/main.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2025 + +//! Main function for the PV Info Tool + +use anyhow::Result; +use clap::Parser; +use std::io::{self, Write}; + +mod cli; +mod constants; +mod handlers; +mod io_utils; +mod pvinfo; +mod se_status; +mod strings; + +use crate::cli::{CliOptions, Commands, OutputFormat}; +use crate::constants::*; +use crate::handlers::handle_supported_flags; +use crate::io_utils::check_uv_exists; +use crate::pvinfo::PvInfo; +use std::path::PathBuf; + +fn main() -> Result<()> { + // Parse CLI arguments and apply post-processing + let mut cli = CliOptions::parse(); + cli.post_process(); + let base_dir = PathBuf::from(BASE_DIR); + let query_dir = base_dir.join(QUERY_DIR); + check_uv_exists()?; + let mut stdout = io::stdout(); + if cli.version { + utils::print_version!(2025); + return Ok(()); + } + match &cli.command { + // Handle the supported-flags subcommand + Some(Commands::SupportedFlags { + secret, + attestation, + header, + }) => { + handle_supported_flags(&mut stdout, *secret, *attestation, *header, query_dir)?; + } + None => { + let data = PvInfo::read(&cli, &base_dir, &query_dir); + // Print output in the requested format + match cli.format { + OutputFormat::Human => data.write(&mut stdout)?, + OutputFormat::Yaml => write!(stdout, "{}", serde_yaml::to_string(&data).unwrap())?, + } + } + } + + Ok(()) +} diff --git a/rust/pvinfo/src/pvinfo.rs b/rust/pvinfo/src/pvinfo.rs new file mode 100644 index 00000000..116b0452 --- /dev/null +++ b/rust/pvinfo/src/pvinfo.rs @@ -0,0 +1,447 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2025 + +//! PV Info method implementation for the PV Info Tool + +use crate::cli::CliOptions; +use crate::constants::*; +use crate::io_utils::{ + collect_bit_messages, collect_limits, collect_version_flags, read_bool_from_file, + read_hex_from_file, +}; +use crate::se_status::*; +use crate::strings::*; +use serde::Serialize; +use std::collections::HashMap; +use std::fmt; +use std::path::Path; + +#[derive(Serialize)] +pub struct PvInfo { + pub se_status: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub facilities: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub feature_indications: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub limits: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_plaintext_control_flags: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_se_header_versions: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_plaintext_attestation_flags: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_attestation_request_versions: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_secret_types: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_add_secret_request_versions: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_plaintext_add_secret_flags: Option>, +} + +impl PvInfo { + pub fn read(cli: &CliOptions, base_dir: &Path, query_dir: &Path) -> Self { + // helper function + fn process_flag_file( + enabled: bool, + query_dir: &Path, + file: &str, + collector: F, + ) -> Option> + where + F: Fn(u64) -> Vec, + { + if !enabled { + return None; + } + + read_hex_from_file(query_dir.join(file), file) + .map(collector) + .filter(|v| !v.is_empty()) + } + + // Read Secure Execution guest or host state + let virt_guest = read_bool_from_file(base_dir.join(PROT_VIRT_GUEST), PROT_VIRT_GUEST); + let virt_host = read_bool_from_file(base_dir.join(PROT_VIRT_HOST), PROT_VIRT_HOST); + let se_status = if cli.se_status { + Some(SeStatus::from_flags(virt_guest, virt_host)) + } else { + None + }; + + // Collect all optional subsections depending on CLI flags + let facilities = process_flag_file(cli.facilities, query_dir, FACILITIES, |hex| { + collect_bit_messages(hex, FACILITIES_DESC) + }); + + let feature_indications = process_flag_file( + cli.feature_indications, + query_dir, + FEATURE_INDICATIONS, + |hex| collect_bit_messages(hex, FEATURE_INDICATIONS_DESC), + ); + + let limits = if cli.limits { + let map = collect_limits(query_dir); + if map.is_empty() { + None + } else { + Some(map) + } + } else { + None + }; + + let supported_plaintext_attestation_flags = process_flag_file( + cli.supported_plaintext_attestation_flags, + query_dir, + SUPP_ATT_PFLAGS, + |hex| collect_bit_messages(hex, SUPP_ATT_PFLAGS_DESC), + ); + + let supported_se_header_versions = process_flag_file( + cli.supported_se_header_versions, + query_dir, + SUPP_SE_HDR_VER, + collect_version_flags, + ); + + let supported_secret_types = process_flag_file( + cli.supported_secret_types, + query_dir, + SUPP_SECRET_TYPES, + |hex| collect_bit_messages(hex, SUPP_SECRET_TYPES_DESC), + ); + + let supported_plaintext_control_flags = process_flag_file( + cli.supported_plaintext_control_flags, + query_dir, + SUPP_SE_HDR_PCF, + |hex| collect_bit_messages(hex, SUPP_SE_HDR_PCF_DESC), + ); + + let supported_attestation_request_versions = process_flag_file( + cli.supported_attestation_request_versions, + query_dir, + SUPP_ATT_REQ_HDR_VER, + collect_version_flags, + ); + + let supported_add_secret_request_versions = process_flag_file( + cli.supported_add_secret_request_versions, + query_dir, + SUPP_ADD_SECRET_REQ_VER, + collect_version_flags, + ); + + let supported_plaintext_add_secret_flags = process_flag_file( + cli.supported_plaintext_add_secret_flags, + query_dir, + SUPP_ADD_SECRET_PCF, + |hex| collect_bit_messages(hex, SUPP_ADD_SECRET_PCF_DESC), + ); + + Self { + se_status, + facilities, + feature_indications, + limits, + supported_plaintext_control_flags, + supported_se_header_versions, + supported_plaintext_attestation_flags, + supported_attestation_request_versions, + supported_secret_types, + supported_add_secret_request_versions, + supported_plaintext_add_secret_flags, + } + } + + //// Print the PvInfo data to the provided writer. + /// Prints SE status first (if present) + /// and then the rest of the sections. + pub fn write(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { + if let Some(status) = &self.se_status { + writeln!(writer, "{status}")?; + } + write!(writer, "{self}")?; + Ok(()) + } +} + +// Implements human-readable output for PvInfo +impl fmt::Display for PvInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(fac) = &self.facilities { + writeln!(f, "\nFacilities: Installed Ultravisor Calls")?; + for entry in fac { + writeln!(f, "{entry}")?; + } + } + + if let Some(fi) = &self.feature_indications { + writeln!(f, "\nFeature Indications: Ultravisor Features")?; + for entry in fi { + writeln!(f, "{entry}")?; + } + } + + if let Some(lims) = &self.limits { + writeln!(f, "\nLimits:")?; + for (k, v) in lims { + writeln!(f, "{k} {v}")?; + } + } + + if let Some(flags) = &self.supported_plaintext_attestation_flags { + writeln!(f, "\nSupported Plaintext Attestation Flags:")?; + for flag in flags { + writeln!(f, "{flag}")?; + } + } + + if let Some(vers) = &self.supported_se_header_versions { + writeln!(f, "\nSupported SE Header Versions:")?; + for ver in vers { + writeln!(f, "{ver}")?; + } + } + + if let Some(types) = &self.supported_secret_types { + writeln!(f, "\nSupported secret types:")?; + for ty in types { + writeln!(f, "{ty}")?; + } + } + + if let Some(flags) = &self.supported_plaintext_control_flags { + writeln!(f, "\nSupported plaintext control flags:")?; + for flag in flags { + writeln!(f, "{flag}")?; + } + } + + if let Some(vers) = &self.supported_attestation_request_versions { + writeln!(f, "\nSupported Attestation Request Versions:")?; + for ver in vers { + writeln!(f, "{ver}")?; + } + } + + if let Some(vers) = &self.supported_add_secret_request_versions { + writeln!(f, "\nSupported Add Secret Request Versions:")?; + for ver in vers { + writeln!(f, "{ver}")?; + } + } + + if let Some(flags) = &self.supported_plaintext_add_secret_flags { + writeln!(f, "\nSupported plaintext add secret flags:")?; + for flag in flags { + writeln!(f, "{flag}")?; + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + //! Unit Tests for pvinfo_method + + use super::*; + use crate::se_status::SeStatus; + use std::collections::HashMap; + + // Test that Display for PvInfo produces a truly empty string + // when all optional fields are None/ + + #[test] + fn display_with_no_optional_fields_is_empty() { + // Create a PvInfo instance with only se_status set, everything else is None + let pv = PvInfo { + se_status: Some(SeStatus::from_flags(false, false)), + facilities: None, + feature_indications: None, + limits: None, + supported_plaintext_control_flags: None, + supported_se_header_versions: None, + supported_plaintext_attestation_flags: None, + supported_attestation_request_versions: None, + supported_secret_types: None, + supported_add_secret_request_versions: None, + supported_plaintext_add_secret_flags: None, + }; + + // Format PvInfo into a string + let out = format!("{pv}"); + + // Assert that when all optional fields are None, Display implementation outputs nothing + assert!( + out.trim().is_empty(), + "expected empty display, got: {out:?}" + ); + } + + // Test that Display for PvInfo containing all fields + // produces the correct headers and entries for each section. + // This verifies both section titles and specific values appear in the output + + #[test] + fn display_with_all_fields_contains_expected_sections_and_entries() { + // Create a limits map with multiple entries + let mut limits = HashMap::new(); + limits.insert("max_secrets".to_string(), 42_u64); + limits.insert("max_payload".to_string(), 1024_u64); + + // Create a PvInfo instance with all fields populated + let pv = PvInfo { + se_status: Some(SeStatus::from_flags(true, false)), + facilities: Some(vec!["fac-a".into(), "fac-b".into()]), + feature_indications: Some(vec!["feat-x".into(), "feat-y".into()]), + limits: Some(limits), + supported_plaintext_control_flags: Some(vec!["pcf-1".into()]), + supported_se_header_versions: Some(vec!["v1".into(), "v2".into()]), + supported_plaintext_attestation_flags: Some(vec!["paf-1".into()]), + supported_attestation_request_versions: Some(vec!["arv-1".into()]), + supported_secret_types: Some(vec!["secret-type-foo".into()]), + supported_add_secret_request_versions: Some(vec!["asrv-1".into()]), + supported_plaintext_add_secret_flags: Some(vec!["pasf-1".into()]), + }; + + // Format PvInfo into a string + let out = format!("{pv}"); + + // Verify that all expected section headers appear in the formatted output + assert!( + out.contains("Facilities: Installed Ultravisor Calls"), + "missing facilities header: {out}" + ); + assert!( + out.contains("Feature Indications: Ultravisor Features"), + "missing feature indications header: {out}" + ); + assert!(out.contains("Limits:"), "missing Limits header: {out}"); + assert!( + out.contains("Supported Plaintext Attestation Flags:"), + "missing attestation flags header: {out}" + ); + assert!( + out.contains("Supported SE Header Versions:"), + "missing se header versions header: {out}" + ); + assert!( + out.contains("Supported secret types:"), + "missing secret types header: {out}" + ); + assert!( + out.contains("Supported plaintext control flags:"), + "missing plaintext control flags header: {out}" + ); + assert!( + out.contains("Supported Attestation Request Versions:"), + "missing attestation request versions header: {out}" + ); + assert!( + out.contains("Supported Add Secret Request Versions:"), + "missing add secret request versions header: {out}" + ); + assert!( + out.contains("Supported plaintext add secret flags:"), + "missing plaintext add secret flags header: {out}" + ); + + // Verify that specific entries inside sections are correctly displayed + assert!(out.contains("fac-a"), "missing facility entry: {out}"); + assert!( + out.contains("feat-x"), + "missing feature indication entry: {out}" + ); + assert!( + out.contains("max_secrets 42"), + "missing limits key/value: {out}" + ); + assert!( + out.contains("max_payload 1024"), + "missing limits key/value: {out}" + ); + assert!( + out.contains("pcf-1"), + "missing plaintext control flag entry: {out}" + ); + assert!(out.contains("v1"), "missing se header version entry: {out}"); + assert!( + out.contains("secret-type-foo"), + "missing secret type entry: {out}" + ); + assert!( + out.contains("pasf-1"), + "missing add secret flag entry: {out}" + ); + } + + // Test that serde serialization of `PvInfo` skips fields set to None + // Ensures optional values are omitted in the YAML output, but mandatory fields remain + #[test] + fn serde_serialization_skips_none_fields() { + // Create a PvInfo instance with some fields set, most left as None + let pv = PvInfo { + se_status: Some(SeStatus::from_flags(false, true)), + facilities: Some(vec!["one".into()]), + feature_indications: None, + limits: None, + supported_plaintext_control_flags: None, + supported_se_header_versions: None, + supported_plaintext_attestation_flags: None, + supported_attestation_request_versions: None, + supported_secret_types: None, + supported_add_secret_request_versions: None, + supported_plaintext_add_secret_flags: None, + }; + + // Serialize PvInfo into YAML + let v = serde_yaml::to_value(pv).expect("serialize to value"); + + // Verify YAML is a mapping (equivalent to an object in JSON) + let map = v.as_mapping().expect("expected YAML mapping"); + + // "facilities" should appear because it's Some(...) + assert!( + map.contains_key(serde_yaml::Value::from("facilities")), + "facilities should be serialized when Some" + ); + + // Optional fields that were None should not be serialized + assert!( + !map.contains_key(serde_yaml::Value::from("feature_indications")), + "feature_indications should be skipped when None" + ); + assert!( + !map.contains_key(serde_yaml::Value::from("limits")), + "limits should be skipped when None" + ); + assert!( + !map.contains_key(serde_yaml::Value::from("supported_plaintext_control_flags")), + "supported_plaintext_control_flags should be skipped when None" + ); + + // "se_status" should always be present + assert!( + map.contains_key(serde_yaml::Value::from("se_status")), + "se_status should be serialized" + ); + } +} diff --git a/rust/pvinfo/src/se_status.rs b/rust/pvinfo/src/se_status.rs new file mode 100644 index 00000000..adea23fe --- /dev/null +++ b/rust/pvinfo/src/se_status.rs @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2025 + +//! se-status definitions for the PV Info Tool + +use serde::Serialize; // Trait for serializing data structures (e.g., YAML) +use std::fmt; // Provides the `Display` trait for pretty-printing + +/// Enum representing Secure Execution status + +#[derive(Serialize, PartialEq, Debug)] // Automatically implements `serde::Serialize` +pub enum SeStatus { + /// Running as a Secure Execution Guest + Guest, + + /// Running as a Secure Execution Host + Host, + + /// Secure Execution is not enabled + Unsecure, + + /// Invalid state: Secure Execution is enabled as both Guest and Host + /// This state is impossible in practice + Invalid, +} + +// Implement the `Display` trait so we can print human-readable strings instead of enum names. + +impl fmt::Display for SeStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Map each enum variant to a descriptive string + let s = match self { + SeStatus::Guest => "Secure Execution Guest", + SeStatus::Host => "Secure Execution Host", + SeStatus::Unsecure => "Secure Execution is not enabled", + SeStatus::Invalid => "Invalid state: both guest and host enabled (impossible)", + }; + + // Write the chosen string into the formatter + write!(f, "{s}") + } +} + +// Associated functions (like static methods) for constructing SeStatus values. + +impl SeStatus { + /// Construct a `SeStatus` from two boolean flags: + /// - `virt_guest`: true if running as a guest + /// - `virt_host`: true if running as a host + /// + /// Returns the correct variant of `SeStatus`. + pub fn from_flags(virt_guest: bool, virt_host: bool) -> Self { + match (virt_guest, virt_host) { + (true, false) => SeStatus::Guest, + (false, true) => SeStatus::Host, + (false, false) => SeStatus::Unsecure, + (true, true) => SeStatus::Invalid, + } + } +} + +#[cfg(test)] +mod test { + //! Unit Tests for se_status + + use super::SeStatus; + + // Display implementation tests + // Verifies that each enum variant of SeStatus is converted into the expected human-readable string + // via the Display trait implementation + + #[test] + fn test_display_strings() { + assert_eq!(SeStatus::Guest.to_string(), "Secure Execution Guest"); + assert_eq!(SeStatus::Host.to_string(), "Secure Execution Host"); + assert_eq!( + SeStatus::Unsecure.to_string(), + "Secure Execution is not enabled" + ); + assert_eq!( + SeStatus::Invalid.to_string(), + "Invalid state: both guest and host enabled (impossible)" + ); + } + + // from_flags() constructor tests + // Ensures that from_flags() correctly maps the + // combinations of (virt_guest, virt_host) booleans + // into the expected SeStatus variant + + #[test] + fn test_from_flags_variants() { + assert_eq!(SeStatus::from_flags(true, false), SeStatus::Guest); + assert_eq!(SeStatus::from_flags(false, true), SeStatus::Host); + assert_eq!(SeStatus::from_flags(false, false), SeStatus::Unsecure); + assert_eq!(SeStatus::from_flags(true, true), SeStatus::Invalid); + } +} diff --git a/rust/pvinfo/src/strings.rs b/rust/pvinfo/src/strings.rs new file mode 100644 index 00000000..f5edc8e3 --- /dev/null +++ b/rust/pvinfo/src/strings.rs @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2025 + +//! For processing descriptions + +pub const FACILITIES_DESC: &str = r#"Query Ultravisor Information +Initialize Ultravisor +Create Secure Configuration +Destroy Secure Configuration +Create Secure CPU +Destroy Secure CPU +Convert to Secure Storage +Convert from Secure Storage +Set Shared Access +Remove Shared Access +Reserved +Set Secure Parameters +Destroy Secure Storage +Unpack Image +Verify Image +Perform CPU Reset +Perform Initial CPU Reset +Set CPU State +Prepare for Reset +Perform CPU Clear Reset +Unshare All +Pin Shared Storage +Unpin Shared Storage +Destroy Secure Configuration Fast +Initiate Configuration Dump +Dump Configuration Storage State +Dump CPU State +Complete Configuration Dump +Retrieve Attestation Measurement +Add Secret +List Secrets +Lock Secrets +Verify Large Frame +Retrieve Secret"#; + +pub const FEATURE_INDICATIONS_DESC: &str = r#"Reserved +Adapter interrupt virtualization is supported +Reserved +Reserved +AP passthrough is supported +AP interpretion passthrough is supported"#; + +pub const SUPP_ADD_SECRET_PCF_DESC: &str = r#"Disable dumping"#; + +pub const SUPP_ATT_PFLAGS_DESC: &str = r#"Reserved +The attestation request contains an optional nonce +Adding the SHA-256 hash of the public host key to the additional data area for measurement. +Adding the SHA-256 hash of the public host key for the Attestation request header to the additional data area for measurement. +The Add-secret Request Stream Flag (ARSF) is a SHA-512 hash of successful add-secret tags (in order), plus a byte indicating store lock status. +Adding the 320-byte firmware attestation measurement (FWCF) to the additional data area."#; + +pub const SUPP_SE_HDR_PCF_DESC: &str = r#"Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Dumping of the secure execution guest is allowed +The decrypt image ultravisor command does not decrypt the content of the specified 4K-byte block of storage. The page-list digest, the address-list digest, and the tweak-list digest are still verified. +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +PCKMO encrypt-DEA/TDEA-key functions allowed +PCKMO encrypt-AES-key functions allowed +PCKMO encrypt-ECC-key functions allowed +Reserved +Reserved +Reserved +Temporary backup-host-key use allowed +Reserved"#; + +pub const SUPP_SECRET_TYPES_DESC: &str = r#"Reserved +Meta +AP-association +Plaintext +AES 128 +AES 192 +AES 256 +AES 128 XTS +AES 256 XTS +HMAC SHA 256 +HMAC SHA 512 +Reserved +Reserved +Reserved +Reserved +Reserved +Reserved +ECDSA P256 private key +ECDSA P384 private key +ECDSA P521 private key +EdDSA Ed25529 private key +EdDSA Ed448 private key +Update-CCK"#; diff --git a/rust/pvinfo/tests/assets/prot_virt_guest b/rust/pvinfo/tests/assets/prot_virt_guest new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/rust/pvinfo/tests/assets/prot_virt_guest @@ -0,0 +1 @@ +0 diff --git a/rust/pvinfo/tests/assets/prot_virt_host b/rust/pvinfo/tests/assets/prot_virt_host new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/rust/pvinfo/tests/assets/prot_virt_host @@ -0,0 +1 @@ +1 diff --git a/rust/pvinfo/tests/assets/query/dump_finalize_len b/rust/pvinfo/tests/assets/query/dump_finalize_len new file mode 100644 index 00000000..83b33d23 --- /dev/null +++ b/rust/pvinfo/tests/assets/query/dump_finalize_len @@ -0,0 +1 @@ +1000 diff --git a/rust/pvinfo/tests/assets/query/dump_storage_state_len b/rust/pvinfo/tests/assets/query/dump_storage_state_len new file mode 100644 index 00000000..83b33d23 --- /dev/null +++ b/rust/pvinfo/tests/assets/query/dump_storage_state_len @@ -0,0 +1 @@ +1000 diff --git a/rust/pvinfo/tests/assets/query/facilities b/rust/pvinfo/tests/assets/query/facilities new file mode 100644 index 00000000..814d766e --- /dev/null +++ b/rust/pvinfo/tests/assets/query/facilities @@ -0,0 +1,4 @@ +ff1ffff000000000 +0 +0 +0 diff --git a/rust/pvinfo/tests/assets/query/feature_indications b/rust/pvinfo/tests/assets/query/feature_indications new file mode 100644 index 00000000..e60610de --- /dev/null +++ b/rust/pvinfo/tests/assets/query/feature_indications @@ -0,0 +1 @@ +fc00000000000000 diff --git a/rust/pvinfo/tests/assets/query/max_address b/rust/pvinfo/tests/assets/query/max_address new file mode 100644 index 00000000..5702cf9c --- /dev/null +++ b/rust/pvinfo/tests/assets/query/max_address @@ -0,0 +1 @@ +800000000000 diff --git a/rust/pvinfo/tests/assets/query/max_assoc_secrets b/rust/pvinfo/tests/assets/query/max_assoc_secrets new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/rust/pvinfo/tests/assets/query/max_assoc_secrets @@ -0,0 +1 @@ +0 diff --git a/rust/pvinfo/tests/assets/query/max_cpus b/rust/pvinfo/tests/assets/query/max_cpus new file mode 100644 index 00000000..a700e799 --- /dev/null +++ b/rust/pvinfo/tests/assets/query/max_cpus @@ -0,0 +1 @@ +257 diff --git a/rust/pvinfo/tests/assets/query/max_guests b/rust/pvinfo/tests/assets/query/max_guests new file mode 100644 index 00000000..1af3e757 --- /dev/null +++ b/rust/pvinfo/tests/assets/query/max_guests @@ -0,0 +1 @@ +8388607 diff --git a/rust/pvinfo/tests/assets/query/max_retr_secrets b/rust/pvinfo/tests/assets/query/max_retr_secrets new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/rust/pvinfo/tests/assets/query/max_retr_secrets @@ -0,0 +1 @@ +0 diff --git a/rust/pvinfo/tests/assets/query/max_secrets b/rust/pvinfo/tests/assets/query/max_secrets new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/rust/pvinfo/tests/assets/query/max_secrets @@ -0,0 +1 @@ +0 diff --git a/rust/pvinfo/tests/assets/query/supp_add_secret_pcf b/rust/pvinfo/tests/assets/query/supp_add_secret_pcf new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/rust/pvinfo/tests/assets/query/supp_add_secret_pcf @@ -0,0 +1 @@ +0 diff --git a/rust/pvinfo/tests/assets/query/supp_add_secret_req_ver b/rust/pvinfo/tests/assets/query/supp_add_secret_req_ver new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/rust/pvinfo/tests/assets/query/supp_add_secret_req_ver @@ -0,0 +1 @@ +0 diff --git a/rust/pvinfo/tests/assets/query/supp_att_pflags b/rust/pvinfo/tests/assets/query/supp_att_pflags new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/rust/pvinfo/tests/assets/query/supp_att_pflags @@ -0,0 +1 @@ +0 diff --git a/rust/pvinfo/tests/assets/query/supp_att_req_hdr_ver b/rust/pvinfo/tests/assets/query/supp_att_req_hdr_ver new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/rust/pvinfo/tests/assets/query/supp_att_req_hdr_ver @@ -0,0 +1 @@ +0 diff --git a/rust/pvinfo/tests/assets/query/supp_se_hdr_pcf b/rust/pvinfo/tests/assets/query/supp_se_hdr_pcf new file mode 100644 index 00000000..5db6246e --- /dev/null +++ b/rust/pvinfo/tests/assets/query/supp_se_hdr_pcf @@ -0,0 +1 @@ +300050e3 diff --git a/rust/pvinfo/tests/assets/query/supp_se_hdr_ver b/rust/pvinfo/tests/assets/query/supp_se_hdr_ver new file mode 100644 index 00000000..88c214eb --- /dev/null +++ b/rust/pvinfo/tests/assets/query/supp_se_hdr_ver @@ -0,0 +1 @@ +8000000000000000 diff --git a/rust/pvinfo/tests/assets/query/supp_secret_types b/rust/pvinfo/tests/assets/query/supp_secret_types new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/rust/pvinfo/tests/assets/query/supp_secret_types @@ -0,0 +1 @@ +0 diff --git a/rust/pvinfo/tests/assets/query/uv_query_dump_cpu_len b/rust/pvinfo/tests/assets/query/uv_query_dump_cpu_len new file mode 100644 index 00000000..83b33d23 --- /dev/null +++ b/rust/pvinfo/tests/assets/query/uv_query_dump_cpu_len @@ -0,0 +1 @@ +1000