mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
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 <seiden@linux.ibm.com> Signed-off-by: Serena Santosh <serena@linux.ibm.com> Signed-off-by: Ann Mariya Jojo <annjojo@linux.ibm.com> Signed-off-by: Annu Sharma <annu09@linux.ibm.com> Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
This commit is contained in:
committed by
Steffen Eiden
parent
c0d8034962
commit
ce82010827
14
rust/Cargo.lock
generated
14
rust/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -6,6 +6,7 @@ members = [
|
||||
"pvapconfig",
|
||||
"pvattest",
|
||||
"pvimg",
|
||||
"pvinfo",
|
||||
"pvsecret",
|
||||
"utils",
|
||||
]
|
||||
|
||||
@@ -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)"
|
||||
|
||||
|
||||
25
rust/pvinfo/Cargo.toml
Normal file
25
rust/pvinfo/Cargo.toml
Normal file
@@ -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"
|
||||
26
rust/pvinfo/build.rs
Normal file
26
rust/pvinfo/build.rs
Normal file
@@ -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(())
|
||||
}
|
||||
230
rust/pvinfo/src/cli.rs
Normal file
230
rust/pvinfo/src/cli.rs
Normal file
@@ -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<Commands>,
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
}
|
||||
43
rust/pvinfo/src/constants.rs
Normal file
43
rust/pvinfo/src/constants.rs
Normal file
@@ -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"),
|
||||
];
|
||||
190
rust/pvinfo/src/handlers.rs
Normal file
190
rust/pvinfo/src/handlers.rs
Normal file
@@ -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<String> {
|
||||
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<Vec<String>> {
|
||||
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::<Vec<_>>()
|
||||
.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!"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
320
rust/pvinfo/src/io_utils.rs
Normal file
320
rust/pvinfo/src/io_utils.rs
Normal file
@@ -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<P: AsRef<Path>>(path: P, context: &str) -> Option<u64> {
|
||||
// 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<P: AsRef<Path>>(path: P, context: &str) -> Option<u64> {
|
||||
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::<u64>() {
|
||||
Ok(val) => Some(val),
|
||||
Err(e) => panic!(
|
||||
"Failed to parse integer in {} ({}): {}",
|
||||
path.as_ref().display(),
|
||||
context,
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_bool_from_file<P: AsRef<Path>>(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<String> {
|
||||
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<String> {
|
||||
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<String, u64> {
|
||||
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::<io::Error>() {
|
||||
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-<index>" 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()));
|
||||
}
|
||||
}
|
||||
58
rust/pvinfo/src/main.rs
Normal file
58
rust/pvinfo/src/main.rs
Normal file
@@ -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(())
|
||||
}
|
||||
447
rust/pvinfo/src/pvinfo.rs
Normal file
447
rust/pvinfo/src/pvinfo.rs
Normal file
@@ -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<SeStatus>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub facilities: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub feature_indications: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub limits: Option<HashMap<String, u64>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_plaintext_control_flags: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_se_header_versions: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_plaintext_attestation_flags: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_attestation_request_versions: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_secret_types: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_add_secret_request_versions: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_plaintext_add_secret_flags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl PvInfo {
|
||||
pub fn read(cli: &CliOptions, base_dir: &Path, query_dir: &Path) -> Self {
|
||||
// helper function
|
||||
fn process_flag_file<F>(
|
||||
enabled: bool,
|
||||
query_dir: &Path,
|
||||
file: &str,
|
||||
collector: F,
|
||||
) -> Option<Vec<String>>
|
||||
where
|
||||
F: Fn(u64) -> Vec<String>,
|
||||
{
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
99
rust/pvinfo/src/se_status.rs
Normal file
99
rust/pvinfo/src/se_status.rs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
145
rust/pvinfo/src/strings.rs
Normal file
145
rust/pvinfo/src/strings.rs
Normal file
@@ -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"#;
|
||||
1
rust/pvinfo/tests/assets/prot_virt_guest
Normal file
1
rust/pvinfo/tests/assets/prot_virt_guest
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
rust/pvinfo/tests/assets/prot_virt_host
Normal file
1
rust/pvinfo/tests/assets/prot_virt_host
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
1
rust/pvinfo/tests/assets/query/dump_finalize_len
Normal file
1
rust/pvinfo/tests/assets/query/dump_finalize_len
Normal file
@@ -0,0 +1 @@
|
||||
1000
|
||||
1
rust/pvinfo/tests/assets/query/dump_storage_state_len
Normal file
1
rust/pvinfo/tests/assets/query/dump_storage_state_len
Normal file
@@ -0,0 +1 @@
|
||||
1000
|
||||
4
rust/pvinfo/tests/assets/query/facilities
Normal file
4
rust/pvinfo/tests/assets/query/facilities
Normal file
@@ -0,0 +1,4 @@
|
||||
ff1ffff000000000
|
||||
0
|
||||
0
|
||||
0
|
||||
1
rust/pvinfo/tests/assets/query/feature_indications
Normal file
1
rust/pvinfo/tests/assets/query/feature_indications
Normal file
@@ -0,0 +1 @@
|
||||
fc00000000000000
|
||||
1
rust/pvinfo/tests/assets/query/max_address
Normal file
1
rust/pvinfo/tests/assets/query/max_address
Normal file
@@ -0,0 +1 @@
|
||||
800000000000
|
||||
1
rust/pvinfo/tests/assets/query/max_assoc_secrets
Normal file
1
rust/pvinfo/tests/assets/query/max_assoc_secrets
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
rust/pvinfo/tests/assets/query/max_cpus
Normal file
1
rust/pvinfo/tests/assets/query/max_cpus
Normal file
@@ -0,0 +1 @@
|
||||
257
|
||||
1
rust/pvinfo/tests/assets/query/max_guests
Normal file
1
rust/pvinfo/tests/assets/query/max_guests
Normal file
@@ -0,0 +1 @@
|
||||
8388607
|
||||
1
rust/pvinfo/tests/assets/query/max_retr_secrets
Normal file
1
rust/pvinfo/tests/assets/query/max_retr_secrets
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
rust/pvinfo/tests/assets/query/max_secrets
Normal file
1
rust/pvinfo/tests/assets/query/max_secrets
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
rust/pvinfo/tests/assets/query/supp_add_secret_pcf
Normal file
1
rust/pvinfo/tests/assets/query/supp_add_secret_pcf
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
rust/pvinfo/tests/assets/query/supp_add_secret_req_ver
Normal file
1
rust/pvinfo/tests/assets/query/supp_add_secret_req_ver
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
rust/pvinfo/tests/assets/query/supp_att_pflags
Normal file
1
rust/pvinfo/tests/assets/query/supp_att_pflags
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
rust/pvinfo/tests/assets/query/supp_att_req_hdr_ver
Normal file
1
rust/pvinfo/tests/assets/query/supp_att_req_hdr_ver
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
rust/pvinfo/tests/assets/query/supp_se_hdr_pcf
Normal file
1
rust/pvinfo/tests/assets/query/supp_se_hdr_pcf
Normal file
@@ -0,0 +1 @@
|
||||
300050e3
|
||||
1
rust/pvinfo/tests/assets/query/supp_se_hdr_ver
Normal file
1
rust/pvinfo/tests/assets/query/supp_se_hdr_ver
Normal file
@@ -0,0 +1 @@
|
||||
8000000000000000
|
||||
1
rust/pvinfo/tests/assets/query/supp_secret_types
Normal file
1
rust/pvinfo/tests/assets/query/supp_secret_types
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
rust/pvinfo/tests/assets/query/uv_query_dump_cpu_len
Normal file
1
rust/pvinfo/tests/assets/query/uv_query_dump_cpu_len
Normal file
@@ -0,0 +1 @@
|
||||
1000
|
||||
Reference in New Issue
Block a user