From 94a38ebc3aec9ee954f1ae9a4a0a09ea7f29f11c Mon Sep 17 00:00:00 2001 From: Harald Freudenberger Date: Fri, 1 Dec 2023 12:10:20 +0100 Subject: [PATCH] rust/pvapconfig: Introduce new tool pvapconfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pvapconfig is a new tool for automatically configuring the APQNs within an Secure Execution KVM guest with AP pass-through support. Based on a given AP configuration it tries to find a matching APQN and bind and associate it with the correct secret. Signed-off-by: Harald Freudenberger Reviewed-by: Ingo Franzki Reviewed-by: Holger Dengler Reviewed-by: Steffen Eiden Signed-off-by: Jan Höppner --- rust/Cargo.lock | 16 + rust/Cargo.toml | 1 + rust/Makefile | 2 +- rust/pvapconfig/Cargo.toml | 19 + rust/pvapconfig/README.md | 9 + rust/pvapconfig/man/pvapconfig.1 | 174 +++++++ rust/pvapconfig/src/ap.rs | 817 +++++++++++++++++++++++++++++++ rust/pvapconfig/src/cli.rs | 63 +++ rust/pvapconfig/src/config.rs | 391 +++++++++++++++ rust/pvapconfig/src/helper.rs | 272 ++++++++++ rust/pvapconfig/src/main.rs | 668 +++++++++++++++++++++++++ rust/pvapconfig/src/uv.rs | 105 ++++ 12 files changed, 2536 insertions(+), 1 deletion(-) create mode 100644 rust/pvapconfig/Cargo.toml create mode 100644 rust/pvapconfig/README.md create mode 100644 rust/pvapconfig/man/pvapconfig.1 create mode 100644 rust/pvapconfig/src/ap.rs create mode 100644 rust/pvapconfig/src/cli.rs create mode 100644 rust/pvapconfig/src/config.rs create mode 100644 rust/pvapconfig/src/helper.rs create mode 100644 rust/pvapconfig/src/main.rs create mode 100644 rust/pvapconfig/src/uv.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 067be057..3a6c4238 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -792,6 +792,22 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pvapconfig" +version = "0.9.0" +dependencies = [ + "clap", + "lazy_static", + "openssl", + "openssl-sys", + "pv", + "rand", + "regex", + "serde", + "serde_yaml", + "utils", +] + [[package]] name = "pvsecret" version = "0.9.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 7ba1faa1..f358dbd9 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "pv", + "pvapconfig", "pvsecret", "utils", ] diff --git a/rust/Makefile b/rust/Makefile index fa3cf04d..818694d0 100644 --- a/rust/Makefile +++ b/rust/Makefile @@ -19,7 +19,7 @@ ifneq (${HAVE_CARGO},0) ifneq (${HAVE_OPENSSL},0) ifneq (${HAVE_LIBCURL},0) - PV_TARGETS := pvsecret + PV_TARGETS := pvsecret pvapconfig PV_BUILD_TARGETS := $(PV_TARGETS) CARGO_TEST_TARGETS += $(addsuffix .test,pv $(PV_TARGETS)) diff --git a/rust/pvapconfig/Cargo.toml b/rust/pvapconfig/Cargo.toml new file mode 100644 index 00000000..2a69468c --- /dev/null +++ b/rust/pvapconfig/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pvapconfig" +description = "A tool to configure the AP resources inside a SE guest based on UV secrets and an AP config file." +authors = ["Harald Freudenberger "] +version = "0.9.0" +edition.workspace = true +license.workspace = true + +[dependencies] +clap = { version ="4.1", features = ["derive", "wrap_help"]} +lazy_static = "1.1" +openssl = { version = "0.10" } +openssl-sys = { version = "0.9" } +pv = { path = "../pv", features = ["uvsecret", "request"] } +rand = "0.8" +regex = "1" +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9" +utils = { path = "../utils" } diff --git a/rust/pvapconfig/README.md b/rust/pvapconfig/README.md new file mode 100644 index 00000000..d30cb76d --- /dev/null +++ b/rust/pvapconfig/README.md @@ -0,0 +1,9 @@ + +# pvapconfig +## Description +**pvapconfig** is used to automatically set up the AP configuration +within an IBM Secure Execution guest. diff --git a/rust/pvapconfig/man/pvapconfig.1 b/rust/pvapconfig/man/pvapconfig.1 new file mode 100644 index 00000000..73ce8cea --- /dev/null +++ b/rust/pvapconfig/man/pvapconfig.1 @@ -0,0 +1,174 @@ +.\" pvapconfig.1 +.\" +.\" Copyright 2023 IBM Corp. +.\" s390-tools is free software; you can redistribute it and/or modify +.\" it under the terms of the MIT license. See LICENSE for details. +.\" +.\" use +.\" groff -man -Tutf8 pvapconfig.1 +.\" or +.\" nroff -man pvapconfig.1 +.\" to process this source +.\" +.TH PVAPCONFIG 1 "DEC 2023" "s390-tools" +.SH NAME +pvapconfig \- automatic configure APQNs within an SE KVM guest. +.SH SYNOPSIS +.TP 9 +.B pvapconfig [OPTIONS] +.SH DESCRIPTION +pvapconfig is a tool for automatically configuring the APQNs within an +Secure Execution KVM guest with AP pass-through support. Based on a +given AP configuration it tries to find matching APQNs and binds and +associates them with the given secret(s). + +Here is a description of pvapconfig's process: +.TP 3 +1. Check AP bus: Support for AP bus needs to be available and the AP +bus needs to support APSB. APSB is only available within an KVM SE +guest with AP pass-through support. +.TP 3 +2. Check Ultravisor: UV support needs to be available and the UV needs +to support the +.I list secrets +feature. +.TP 3 +3. Read in and validate the AP configuration file. By default if not +overwritten by the --config option the AP configuration is read from +.I /etc/pvapconfig.yaml +and syntactically verified. See section CONFIGFILE for details about +the syntax and semantic of the configuration file. +.TP 3 +4. Fetch the list of association secrets from the UV. Actually the +index of the secret and the secret id for each entry is collected. The +secret value is NOT fetched as it is NOT accessible but only usable +within the UV firmware. +.TP 3 +5. Gather all APQNs available within this KVM SE guest. Collect +information about each APQN like online states, crypto card serial +numbers and master key verification patterns (MKVP). +.TP 3 +6. Go through all AP config entries. For each AP config entry try to +find an APQN which is already configured (bound/associated) to +satisfy this config entry. If such a match is found, the AP config +entry is assumed to be fulfilled and marked as done. +.TP 3 +7. All remaining APQNs which do not already satisfy an AP config entry +are now examined for their bind and association state and maybe reset +to unbound state. +.TP 3 +8. Go through all AP config entries which are still not +fulfilled. For each such AP config entry try to search for an APQN +which would match to this entry and then prepare this APQN (bind, +maybe associate). If successful, mark the AP config entry as done. +.TP 3 +9. Evaluation of the applied AP config entries. Applied means the AP +config entry has been fulfilled either in step 6 or in step 8. With +the strict option given ALL AP config entries need to apply otherwise +an error message is printed and pvapconfig returns with exit failure. +If the strict option is not given, it is enough to satisfy at least one +AP config entry from the configuration and pvapconfig will return +successfully. +.SH OPTIONS +.TP 8 +.B -c, --config +Use as the AP config file for pvapconfig. If pvapconfig +is run without this option the default configuration file +/etc/pvapconfig.yaml is used. +.TP 8 +.B -h, --help +Print pvapconfig usage information and exit. +.TP 8 +.B -n, --dry-run +Do not bind, unbind or associate APQNs but only process the +configuration and the available APQNs and secrets and simulate the +bind, unbind or associate action on the chosen APQN. Use it together +with the verbose option to see which actions pvapconfig would do if +unleashed. +.TP 8 +.B -s, --strict +All AP config entries need to be satisfied to have pvapconfig +terminate with success. Without this option one applied AP config +entry is enough to meet the expectations. +.TP 8 +.B -v, --verbose +Print out informational messages about what pvapconfig is actually +doing. +.TP 8 +.B -V, --version +Print version information and exit. +.SH CONFIGFILE +The pvapconfig yaml configuration file consists of a list of AP config +entries. Each entry may hold this information: +.TP 2 +- mode: AP queue mode information, required, either "EP11" or "Accel". +.TP 2 +- mkvp: AP queue Master Key Verification Pattern (MKVP), required for +EP11, hex string optional prepented with 0x. The MKVP hex string value +may hold either 16 bytes (32 hex characters) or 32 bytes (64 hex +characters) but only the leftmost 16 bytes hold MKVP information and +thus the rest is ignored. +.TP 2 +- serialnr: Crypto Card Serial Number, string, optional for EP11, +ignored for Accel. As this is a real ASCII string uppercase and +lowercase character(s) count different. +.TP 2 +- mingen: Card Minimal Generation, string "CEX4", "CEX5", "CEX6", +"CEX7" or "CEX8" for Accelerator, string "CEC8" for EP11, optional. If +given specifies the minimal accepted Crypto card generation. +.TP 2 +- secretid: Secret id, hex string with optional 0x prepented, required +for EP11, ignored for Accel. Details see the following text. +.TP 2 +- name: ASCII string, optional, but see details below. +.TP 2 +- description: Description of this AP config entry, string, ignored, + just for convenience for the reader or editor of the configuration. +.PP +The secret id uniquely identifies an association secret. However, it +is a clumsy hex string of 64 characters which represent the readable +sha256 value over the secret's name. So pvapconfig can use the name +instead and calculate the secret id from the name. So the rule is: +.TP 2 +- If name and secretid is given, the secretid needs to match to the +sha256 hash over the given name for this AP config entry. +.TP 2 +- If only name is given then the secretid is calculated with a sha256 + hash over the given name. +.TP 2 +- If only the secretid is given, there is nothing more to do but +verify that the value is a hex string with 64 characters. +.SH LOCKING +Pvapconfig needs to have a consistent view of the AP resources +during lifetime. There must not run multiple instances of pvapconfig +or any manipulations of the AP resources in parallel. To prevent the +execution of multiple pvapconfig instances the lock file +/run/lock/pvapconfig.lock is established. A second instance of +pvapconfig will detect this lock file and terminated with an error +message. If for any reason this file still exists as a leftover from a +previous pvapconfig crash for example, it needs to get removed by +hand. The lock file contains the process id of the pvapconfig process +which created this file. +.SH RETURN VALUE +.TP 8 +.B 0 - Successful termination. +At least one AP config entry has been applied or at least one APQN has +been found in a state matching to one AP config entry. If strict +option is given, ALL AP config entries have been applied. An AP config +entry is applied either by configuring the APQN accordingly or an APQN +has been found which already fulfills the constrains. +.RE +.TP 8 +.B 1 - Failure. +Either some kind of failure happened during processing the +configuration or the configuration could not get applied +successful. In all cases pvapconfig prints out a message to standard +error with details about the failure. Also pvapconfig does NOT reset +the APQNs to the state found at the startup when failing to apply the +configuration. +.SH NOTES +For more information and details see the IBM documentation about +Confidential Computing "Introducing IBM Secure Execution for Linux" +available at https://www.ibm.com/docs/. +.SH SEE ALSO +\fBpvsecret\fR(1), \fBlszcrypt\fR(8), \fBchzcrypt\fR(8) diff --git a/rust/pvapconfig/src/ap.rs b/rust/pvapconfig/src/ap.rs new file mode 100644 index 00000000..0b882359 --- /dev/null +++ b/rust/pvapconfig/src/ap.rs @@ -0,0 +1,817 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2023 +// +//! AP support functions for pvapconfig +// + +use crate::helper::*; +use regex::Regex; +use std::fmt; +use std::path::Path; +use std::slice::Iter; +use std::thread; +use std::time; + +const PATH_SYS_BUS_AP: &str = "/sys/bus/ap"; +const PATH_SYS_BUS_AP_FEATURES: &str = "/sys/bus/ap/features"; +const PATH_SYS_BUS_AP_BINDINGS: &str = "/sys/bus/ap/bindings"; +const PATH_SYS_DEVICES_AP: &str = "/sys/devices/ap"; + +const RE_CARD_DIR: &str = r"^card([[:xdigit:]]{2})$"; +const RE_QUEUE_DIR: &str = r"^([[:xdigit:]]{2})\.([[:xdigit:]]{4})$"; +const RE_CARD_TYPE: &str = r"^CEX([3-8])([ACP])$"; +const RE_EP11_MKVP: &str = r"WK\s+CUR:\s+(\S+)\s+(\S+)"; +const RE_CCA_AES_MKVP: &str = r"AES\s+CUR:\s+(\S+)\s+(\S+)"; +const RE_CCA_APKA_MKVP: &str = r"APKA\s+CUR:\s+(\S+)\s+(\S+)"; + +const SYS_BUS_AP_BINDINGS_POLL_MS: u64 = 500; + +const SYS_BUS_AP_BIND_POLL_MS: u64 = 500; +const SYS_BUS_AP_BIND_TIMEOUT_MS: u64 = 10000; + +const SYS_BUS_AP_ASSOC_POLL_MS: u64 = 500; +const SYS_BUS_AP_ASSOC_TIMEOUT_MS: u64 = 10000; + +/// Check if AP bus support is available. +/// Returns Result with Ok(()) or Err(failurestring). +pub fn check_ap_bus_support() -> Result<(), String> { + if !Path::new(PATH_SYS_BUS_AP).is_dir() { + return Err(format!( + "AP bus support missing (path {PATH_SYS_BUS_AP} is invalid)." + )); + } + Ok(()) +} + +/// Check if AP bus supports APSB. +/// +/// When APSB support is available returns Result +/// with Ok(()) or otherwise Err(failurestring). +pub fn ap_bus_has_apsb_support() -> Result<(), String> { + if !Path::new(PATH_SYS_BUS_AP_FEATURES).is_file() { + return Err(format!( + "AP bus features support missing (file {PATH_SYS_BUS_AP_FEATURES} does not exist)." + )); + } + let features = sysfs_read_string(PATH_SYS_BUS_AP_FEATURES).map_err(|err| { + format!("Failure reading AP bus features from {PATH_SYS_BUS_AP_FEATURES} ({err:?}).") + })?; + match features.find("APSB") { + Some(_) => Ok(()), + None => Err("Missing AP bus feature APSB (SE AP pass-through not enabled ?).".to_string()), + } +} + +/// Wait for AP bus set up all it's devices. +/// +/// This function loops until the AP bus reports that +/// - all AP queue devices have been constructed +/// - and all AP device have been bound to a device driver. +/// This may take some time and even loop forever if there +/// is something wrong with the kernel modules setup. +/// Returns true when AP bus bindings are complete, +/// otherwise false and a message is printed. +/// When AP bus binding complete is not immediately reached +/// and this function needs to loop, about every 5 seconds +/// a message is printed "Waiting for ...". +pub fn wait_for_ap_bus_bindings_complete() -> bool { + let mut counter = 0; + loop { + match sysfs_read_string(PATH_SYS_BUS_AP_BINDINGS) { + Ok(s) => { + if s.contains("complete") { + return true; + } + } + Err(err) => { + eprintln!( + "Failure reading AP bus bindings from {} ({:?}).", + PATH_SYS_BUS_AP_BINDINGS, err + ); + return false; + } + } + thread::sleep(time::Duration::from_millis(SYS_BUS_AP_BINDINGS_POLL_MS)); + counter += 1; + if counter % 10 == 0 { + println!("Waiting for AP bus bindings complete."); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ApqnMode { + Accel, + Ep11, + Cca, +} + +#[derive(Debug, Clone)] +pub struct ApqnInfoAccel { + // empty +} + +#[derive(Debug, Clone)] +pub struct ApqnInfoEp11 { + pub serialnr: String, + pub mkvp: String, // may be an empty string if no WK set +} + +#[derive(Debug, Clone)] +pub struct ApqnInfoCca { + pub serialnr: String, + pub mkvp_aes: String, // may be an empty string if no MK set + pub mkvp_apka: String, // may be an empty string if no MK set +} + +#[derive(Debug, Clone)] +pub enum ApqnInfo { + Accel(ApqnInfoAccel), + Ep11(ApqnInfoEp11), + Cca(ApqnInfoCca), +} + +impl ApqnInfo { + fn accel_info(_carddir: &str, _queuedir: &str) -> Result { + Ok(ApqnInfo::Accel(ApqnInfoAccel {})) + } + + fn cca_info(carddir: &str, queuedir: &str) -> Result { + let serialnr = match sysfs_read_string(&format!("{carddir}/serialnr")) { + Ok(r) => r, + Err(err) => { + return Err(format!( + "Failure reading serialnr from {carddir}/serialnr: {:?}.", + err + )) + } + }; + let mkvps = match sysfs_read_string(&format!("{carddir}/{queuedir}/mkvps")) { + Ok(r) => r, + Err(err) => { + return Err(format!( + "Failure reading mkvps from {carddir}/{queuedir}/mkvps: {:?}.", + err + )) + } + }; + let mut aes_mkvp = String::new(); + let re_cca_aes_mkvp = Regex::new(RE_CCA_AES_MKVP).unwrap(); + if !re_cca_aes_mkvp.is_match(&mkvps) { + return Err(format!( + "APQN {} failure parsing mkvps string '{}'.", + queuedir, mkvps + )); + } else { + let caps = re_cca_aes_mkvp.captures(&mkvps).unwrap(); + let valid = caps.get(1).unwrap().as_str().to_lowercase(); + if valid != "valid" { + eprintln!( + "Warning: APQN {} has no valid AES master key set.", + queuedir + ); + } else { + aes_mkvp = caps.get(2).unwrap().as_str().to_lowercase(); + if aes_mkvp.starts_with("0x") { + aes_mkvp = String::from(&aes_mkvp[2..]); + } + } + } + let mut apka_mkvp = String::new(); + let re_cca_apka_mkvp = Regex::new(RE_CCA_APKA_MKVP).unwrap(); + if !re_cca_apka_mkvp.is_match(&mkvps) { + return Err(format!( + "APQN {} failure parsing mkvps string '{}'.", + queuedir, mkvps + )); + } else { + let caps = re_cca_apka_mkvp.captures(&mkvps).unwrap(); + let valid = caps.get(1).unwrap().as_str().to_lowercase(); + if valid != "valid" { + eprintln!( + "Warning: APQN {} has no valid APKA master key set.", + queuedir + ); + } else { + apka_mkvp = caps.get(2).unwrap().as_str().to_lowercase(); + if apka_mkvp.starts_with("0x") { + apka_mkvp = String::from(&apka_mkvp[2..]); + } + } + } + Ok(ApqnInfo::Cca(ApqnInfoCca { + serialnr, + mkvp_aes: aes_mkvp, + mkvp_apka: apka_mkvp, + })) + } + + fn ep11_info(carddir: &str, queuedir: &str) -> Result { + let serialnr = match sysfs_read_string(&format!("{carddir}/serialnr")) { + Ok(r) => r, + Err(err) => { + return Err(format!( + "Failure reading serialnr from {carddir}/serialnr: {:?}.", + err + )) + } + }; + let mkvps = match sysfs_read_string(&format!("{carddir}/{queuedir}/mkvps")) { + Ok(r) => r, + Err(err) => { + return Err(format!( + "Failure reading mkvps from {carddir}/{queuedir}/mkvps: {:?}.", + err + )) + } + }; + let mut mkvp = String::new(); + let re_ep11_mkvp = Regex::new(RE_EP11_MKVP).unwrap(); + if !re_ep11_mkvp.is_match(&mkvps) { + return Err(format!( + "APQN {} failure parsing mkvps string '{}'.", + queuedir, mkvps + )); + } else { + let caps = re_ep11_mkvp.captures(&mkvps).unwrap(); + let valid = caps.get(1).unwrap().as_str().to_lowercase(); + if valid != "valid" { + eprintln!("Warning: APQN {} has no valid wrapping key set.", queuedir); + } else { + mkvp = caps.get(2).unwrap().as_str().to_lowercase(); + if mkvp.starts_with("0x") { + mkvp = String::from(&mkvp[2..]); + } + if mkvp.len() > 32 { + mkvp = String::from(&mkvp[..32]) + } + } + } + Ok(ApqnInfo::Ep11(ApqnInfoEp11 { serialnr, mkvp })) + } + + fn info(mode: &ApqnMode, carddir: &str, queuedir: &str) -> Result { + match mode { + ApqnMode::Accel => ApqnInfo::accel_info(carddir, queuedir), + ApqnMode::Cca => ApqnInfo::cca_info(carddir, queuedir), + ApqnMode::Ep11 => ApqnInfo::ep11_info(carddir, queuedir), + } + } +} + +#[derive(Debug, Clone)] +pub struct Apqn { + pub name: String, + pub card: u32, + pub domain: u32, + pub gen: u32, + pub mode: ApqnMode, + pub info: Option, +} + +impl fmt::Display for Apqn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({},{})", self.card, self.domain) + } +} + +impl Apqn { + pub fn bind_state(&self) -> Result { + get_apqn_bind_state(self.card, self.domain) + } + + pub fn set_bind_state(&self, state: BindState) -> Result<(), String> { + set_apqn_bind_state(self.card, self.domain, state) + } + + pub fn associate_state(&self) -> Result { + get_apqn_associate_state(self.card, self.domain) + } + + pub fn set_associate_state(&self, state: AssocState) -> Result<(), String> { + set_apqn_associate_state(self.card, self.domain, state) + } +} + +/// Wrapper object around Vector of Apqns +pub struct ApqnList(Vec); + +impl ApqnList { + #[cfg(test)] // only used in test code + pub fn from_apqn_vec(apqns: Vec) -> ApqnList { + ApqnList(apqns) + } + + #[cfg(test)] // only used in test code + pub fn to_apqn_vec(&self) -> Vec { + self.0.clone() + } + + pub fn iter(&self) -> Iter<'_, Apqn> { + self.0.iter() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Scan AP bus devices in sysfs and construct the Apqnlist. + /// + /// The list is a vector of struct Apqn for each APQN found in sysfs + /// which is online and the card type matches to the regular expression + /// RE_CARD_TYPE. + /// On success a vector of struct Apqn is returned. This list may be + /// empty if there are no APQNs available or do not match to the conditions. + /// On failure None is returned. + /// Fatal errors which should never happened like unable to compile a + /// static regular expression will result in calling panic. + /// # Panics + /// Panics if the compilation of a static regular expression fails. + pub fn gather_apqns() -> Option { + let mut apqns: Vec = Vec::new(); + let re_card_type = Regex::new(RE_CARD_TYPE).unwrap(); + let re_queue_dir = Regex::new(RE_QUEUE_DIR).unwrap(); + let card_dirs = + match sysfs_get_list_of_subdirs_matching_regex(PATH_SYS_DEVICES_AP, RE_CARD_DIR) { + Ok(r) => r, + Err(err) => { + eprintln!( + "Failure reading AP devices {} ({:?}).", + PATH_SYS_DEVICES_AP, err + ); + return None; + } + }; + for dir in card_dirs { + let path = format!("{PATH_SYS_DEVICES_AP}/{dir}"); + let card_type = match sysfs_read_string(&format!("{path}/type")) { + Ok(r) => r, + Err(err) => { + eprintln!("Failure reading card type from {} ({:?}).", path, err); + return None; + } + }; + if !re_card_type.is_match(&card_type) { + eprintln!("Failure parsing card type string '{}'.", card_type); + return None; + } + let caps = re_card_type.captures(&card_type).unwrap(); + let gen = caps.get(1).unwrap().as_str().parse::().unwrap(); + let mode = match caps.get(2).unwrap().as_str().parse::().unwrap() { + 'A' => ApqnMode::Accel, + 'C' => ApqnMode::Cca, + 'P' => ApqnMode::Ep11, + _ => panic!("Code inconsistence between regex RE_CARD_TYPE and evaluation code."), + }; + if pv::misc::pv_guest_bit_set() { + // the UV blocks requests to CCA cards within SE guest with + // AP pass-through support. However, filter out CCA cards as these + // cards cause hangs during information gathering. + if mode == ApqnMode::Cca { + continue; + } + } + let queue_dirs = match sysfs_get_list_of_subdirs_matching_regex(&path, RE_QUEUE_DIR) { + Ok(r) => r, + Err(err) => { + eprintln!( + "Failure reading AP queue directories in {} ({:?}).", + path, err + ); + return None; + } + }; + for queue_dir in queue_dirs { + let _online = match sysfs_read_i32(&format!("{path}/{queue_dir}/online")) { + Ok(1) => true, + _ => continue, + }; + let caps = re_queue_dir.captures(&queue_dir).unwrap(); + let cardstr = caps.get(1).unwrap().as_str(); + let card = u32::from_str_radix(cardstr, 16).unwrap(); + let domstr = caps.get(2).unwrap().as_str(); + let dom = u32::from_str_radix(domstr, 16).unwrap(); + // For the mpvk and serialnr to fetch from the APQN within a SE + // guest the APQN needs to be bound to the guest. So if the APQN + // is not bound, temporarily bind it here until the info has + // been retrieved. + let mut tempbound = false; + if pv::misc::pv_guest_bit_set() { + let cbs = match get_apqn_bind_state(card, dom) { + Ok(bs) => bs, + Err(err) => { + eprintln!( + "Error: Failure reading APQN ({},{}) bind state: {}", + card, dom, err + ); + BindState::NotSupported + } + }; + if cbs == BindState::Unbound { + let r = set_apqn_bind_state(card, dom, BindState::Bound); + if r.is_err() { + eprintln!( + "Warning: Failure to temp. bind APQN ({},{}): {}", + card, + dom, + r.unwrap_err() + ); + continue; + } else { + tempbound = true; + } + }; + }; + let info = match ApqnInfo::info(&mode, &path, &queue_dir) { + Err(err) => { + // print the error but continue with info set to None + eprintln!( + "Warning: Failure to gather info for APQN ({},{}): {}", + card, dom, err + ); + None + } + Ok(i) => Some(i), + }; + if tempbound { + let r = set_apqn_bind_state(card, dom, BindState::Unbound); + if r.is_err() { + eprintln!( + "Warning: Failure to unbind temp. bound APQN ({},{}): {}", + card, + dom, + r.unwrap_err() + ); + } + }; + apqns.push(Apqn { + name: queue_dir.clone(), + card, + domain: dom, + gen, + mode: mode.clone(), + info, + }); + } + } + Some(ApqnList(apqns)) + } + + /// Sort this Apqnlist by card generation: + /// newest generation first, older generations last. + pub fn sort_by_gen(&mut self) { + self.0.sort_unstable_by(|a, b| b.gen.cmp(&a.gen)); + } + + /// Check MK restriction + /// + /// Within one card there must not exist 2 APQNs with same + /// MK setup. This rule only applies to EP11 cards. + /// Returns true if this check passed, + /// otherwise false and a message is printed. + pub fn check_mk_restriction(&self) -> bool { + for a1 in self.0.iter() { + for a2 in self.0.iter() { + if a1.card == a2.card + && a1.domain < a2.domain + && a1.mode == ApqnMode::Ep11 + && a1.info.is_some() + && a2.info.is_some() + { + let i1 = match a1.info.as_ref().unwrap() { + ApqnInfo::Ep11(i) => i, + _ => continue, + }; + let i2 = match a2.info.as_ref().unwrap() { + ApqnInfo::Ep11(i) => i, + _ => continue, + }; + if i1.mkvp.is_empty() || i2.mkvp.is_empty() { + continue; + } + if i1.mkvp == i2.mkvp { + eprintln!("APQN {} and APQN {} have same MPVK", a1, a2); + return false; + } + } + } + } + true + } +} + +#[derive(PartialEq, Eq)] +pub enum BindState { + Bound, + Unbound, + NotSupported, +} + +/// Query bind state for this APQN. +/// +/// Returns a BindState enum as defined above or on failure +/// an error string. Does NOT print any error messages. +pub fn get_apqn_bind_state(card: u32, dom: u32) -> Result { + let path = format!( + "{}/card{:02x}/{:02x}.{:04x}/se_bind", + PATH_SYS_DEVICES_AP, card, card, dom + ); + match sysfs_read_string(&path) { + Err(err) => Err(format!( + "Failure reading se_bind attribute for APQN({},{}): {:?}.", + card, dom, err + )), + Ok(str) => match str.as_str() { + "bound" => Ok(BindState::Bound), + "unbound" => Ok(BindState::Unbound), + "-" => Ok(BindState::NotSupported), + _ => Err(format!("Unknown bind state '{str}'.")), + }, + } +} + +/// Bind or unbind an APQN. +/// +/// The action is determined by the BindState given in. +/// But of course only Bound and Unbound is supported - otherwise +/// this function panics! +/// The function actively loops over the bind state until +/// the requested bind state is reached or a timeout has +/// occurred (SYS_BUS_AP_BIND_TIMEOUT_MS). +/// On success () is returned, on failure an error string +/// is returned. Does NOT print any error messages. +/// # Panics +/// Panics if a desired bind state other than Bund or Unbound is given. +pub fn set_apqn_bind_state(card: u32, dom: u32, state: BindState) -> Result<(), String> { + let path = format!( + "{}/card{:02x}/{:02x}.{:04x}/se_bind", + PATH_SYS_DEVICES_AP, card, card, dom + ); + let r = match state { + BindState::Bound => sysfs_write_i32(&path, 1), + BindState::Unbound => sysfs_write_i32(&path, 0), + _ => panic!("set_apqn_bind_state called with invalid BindState."), + }; + if r.is_err() { + return Err(format!( + "Failure writing se_bind attribute for APQN({},{}): {:?}.", + card, + dom, + r.unwrap_err() + )); + } + let mut ms: u64 = 0; + loop { + thread::sleep(time::Duration::from_millis(SYS_BUS_AP_BIND_POLL_MS)); + ms += SYS_BUS_AP_BIND_POLL_MS; + if ms >= SYS_BUS_AP_BIND_TIMEOUT_MS { + break Err(format!( + "Timeout setting APQN({},{}) bind state.", + card, dom + )); + } + let newstate = match get_apqn_bind_state(card, dom) { + Err(err) => return Err(err), + Ok(s) => s, + }; + if newstate == state { + return Ok(()); + } + } +} + +#[derive(PartialEq, Eq)] +pub enum AssocState { + Associated(u16), + AssociationPending, + Unassociated, + NotSupported, +} + +/// Query association state for this APQN. +/// +/// Returns an AssocState enum as defined above or on failure +/// an error string. Does NOT print any error messages. +pub fn get_apqn_associate_state(card: u32, dom: u32) -> Result { + let path = format!( + "{}/card{:02x}/{:02x}.{:04x}/se_associate", + PATH_SYS_DEVICES_AP, card, card, dom + ); + match sysfs_read_string(&path) { + Err(err) => Err(format!( + "Failure reading se_associate attribute for APQN({},{}: {:?}", + card, dom, err + )), + Ok(str) => { + if let Some(prefix) = str.strip_prefix("associated ") { + let value = &prefix.parse::(); + match value { + Ok(v) => Ok(AssocState::Associated(*v)), + Err(_) => Err(format!("Invalid association index in '{str}'.")), + } + } else { + match str.as_str() { + "association pending" => Ok(AssocState::AssociationPending), + "unassociated" => Ok(AssocState::Unassociated), + "-" => Ok(AssocState::NotSupported), + _ => Err(format!("Unknown association state '{str}'.")), + } + } + } + } +} + +fn set_apqn_associate_state_associate(card: u32, dom: u32, idx: u16) -> Result<(), String> { + let path = format!( + "{}/card{:02x}/{:02x}.{:04x}/se_associate", + PATH_SYS_DEVICES_AP, card, card, dom + ); + let r = sysfs_write_i32(&path, idx as i32); + if r.is_err() { + return Err(format!( + "Failure writing se_associate attribute for APQN({},{}): {:?}.", + card, + dom, + r.unwrap_err() + )); + } + let mut ms: u64 = 0; + loop { + thread::sleep(time::Duration::from_millis(SYS_BUS_AP_ASSOC_POLL_MS)); + ms += SYS_BUS_AP_ASSOC_POLL_MS; + if ms >= SYS_BUS_AP_ASSOC_TIMEOUT_MS { + break Err(format!( + "Timeout setting APQN({},{}) association idx {} state.", + card, dom, idx + )); + } + let newstate = match get_apqn_associate_state(card, dom) { + Err(err) => return Err(err), + Ok(s) => s, + }; + if let AssocState::Associated(i) = newstate { + if idx == i { + return Ok(()); + } else { + return Err(format!( + "Failure: APQN({},{}) is associated with {} but it should be {}.", + card, dom, i, idx + )); + } + } + } +} + +fn set_apqn_associate_state_unbind(card: u32, dom: u32) -> Result<(), String> { + let bindpath = format!( + "{}/card{:02x}/{:02x}.{:04x}/se_bind", + PATH_SYS_DEVICES_AP, card, card, dom + ); + let r = sysfs_write_i32(&bindpath, 0); + if r.is_err() { + return Err(format!( + "Failure writing se_bind attribute for APQN({},{}): {:?}.", + card, + dom, + r.unwrap_err() + )); + } + let mut ms: u64 = 0; + loop { + thread::sleep(time::Duration::from_millis(SYS_BUS_AP_ASSOC_POLL_MS)); + ms += SYS_BUS_AP_ASSOC_POLL_MS; + if ms >= SYS_BUS_AP_ASSOC_TIMEOUT_MS { + break Err(format!( + "Timeout setting APQN({},{}) association unbind state.", + card, dom + )); + } + let newstate = match get_apqn_associate_state(card, dom) { + Err(err) => return Err(err), + Ok(s) => s, + }; + if newstate == AssocState::Unassociated { + return Ok(()); + } + } +} + +/// Associate or Unassociate an APQN. +/// +/// The action is determined by the AssocState given in. +/// But of course only Associated and Unassociated is supported +/// otherwise this function panics! +/// The function actively loops over the association state until +/// the requested state is reached or a timeout has +/// occurred (SYS_BUS_AP_ASSOC_TIMEOUT_MS). +/// The unassociate is in fact a unbind. So the code triggers +/// an unbind and then loops over the sysfs se_associate until +/// "unassociated" is reached. +/// On success () is returned, on failure an error string +/// is returned. Does NOT print any error messages. +/// # Panics +/// Panics if a desired bind state other than Associated or +/// Unassociated is given. +pub fn set_apqn_associate_state(card: u32, dom: u32, state: AssocState) -> Result<(), String> { + match state { + AssocState::Associated(idx) => set_apqn_associate_state_associate(card, dom, idx), + AssocState::Unassociated => set_apqn_associate_state_unbind(card, dom), + _ => panic!("set_apqn_associate_state called with invalid AssocState."), + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + // These tests assume, there is an AP bus available + // Also for each APQN which is online, it is assumed + // to have a valid master key set up (for Ep11 and CCA). + + #[test] + fn test_check_ap_bus_support() { + if Path::new(PATH_SYS_BUS_AP).is_dir() { + assert!(check_ap_bus_support().is_ok()); + } else { + assert!(check_ap_bus_support().is_err()); + } + } + #[test] + fn test_check_ap_bus_apsb_support() { + if Path::new(PATH_SYS_BUS_AP).is_dir() { + // if we are inside a secure execution guest the + // apsb check should succeed. Outside an SE guest + // the check should fail. + if pv::misc::pv_guest_bit_set() { + assert!(ap_bus_has_apsb_support().is_ok()); + } else { + assert!(ap_bus_has_apsb_support().is_err()); + } + } else { + assert!(ap_bus_has_apsb_support().is_err()); + } + } + #[test] + fn test_wait_for_ap_bus_bindings_complete() { + let r = wait_for_ap_bus_bindings_complete(); + if Path::new(PATH_SYS_BUS_AP).is_dir() { + assert!(r); + } else { + assert!(!r); + } + } + #[test] + fn test_gather_apqns() { + let r = ApqnList::gather_apqns(); + if Path::new(PATH_SYS_BUS_AP).is_dir() { + assert!(r.is_some()); + // fail if no entries found + let l = r.unwrap(); + let v = l.to_apqn_vec(); + for a in v { + match a.mode { + ApqnMode::Accel => { + // fail if no ApqnInfo is attached + assert!(a.info.is_some()); + } + ApqnMode::Ep11 => { + // fail if no ApqnInfo is attached + assert!(a.info.is_some()); + let info = a.info.unwrap(); + let i = match &info { + ApqnInfo::Ep11(i) => i, + _ => panic!("ApqnInfo attached onto Ep11 APQN is NOT ApqnInfoEp11 ?!?"), + }; + // fail if no serialnr + assert!(i.serialnr.len() > 0); + // mkvp is either empty (no WK set) or has exact 32 characters + assert!(i.mkvp.is_empty() || i.mkvp.len() == 32); + } + ApqnMode::Cca => { + // fail if no ApqnInfo is attached + assert!(a.info.is_some()); + let info = a.info.unwrap(); + let i = match &info { + ApqnInfo::Cca(i) => i, + _ => panic!("ApqnInfo attached onto Cca APQN is NOT ApqnInfoCca ?!?"), + }; + // fail if no serialnr + assert!(i.serialnr.len() > 0); + // aes mkvp is either empty (no MK set) or exact 16 characters + assert!(i.mkvp_aes.is_empty() || i.mkvp_aes.len() == 16); + // apka mkvp is either empty (no MK set) or exact 16 characters + assert!(i.mkvp_apka.is_empty() || i.mkvp_apka.len() == 16); + } + } + } + } else { + assert!(r.is_none()); + } + } +} diff --git a/rust/pvapconfig/src/cli.rs b/rust/pvapconfig/src/cli.rs new file mode 100644 index 00000000..e7fd2833 --- /dev/null +++ b/rust/pvapconfig/src/cli.rs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2023 +// +//! Command line interface for pvapconfig +// + +use clap::Parser; +use lazy_static::lazy_static; + +/// The default pvapconfig config file +pub const PATH_DEFAULT_CONFIG_FILE: &str = "/etc/pvapconfig.yaml"; + +#[derive(Parser, Clone)] +pub struct Cli { + /// Provide a custom config file (overwrites default /etc/pvapconfig.yaml). + #[arg(short, long, value_name = "FILE")] + pub config: Option, + + /// Dry run: display the actions but don't actually perform them on the APQNs. + #[arg(short = 'n', long = "dry-run")] + pub dryrun: bool, + + /// Enforce strict match: All config entries need to be fullfilled. + /// + /// By default it is enough to successfully apply at least one config entry. + /// With the strict flag enabled, all config entries within a config file + /// need to be applied successful. + #[arg(long = "strict")] + pub strict: bool, + + /// Provide more detailed output. + #[arg(short, long)] + pub verbose: bool, + + /// Print version information and exit. + #[arg(short = 'V', long)] + pub version: bool, +} + +lazy_static! { + pub static ref ARGS: Cli = Cli::parse(); +} + +impl Cli { + /// verbose returns true if the verbose command line option + /// was given, otherwise false is returned. + pub fn verbose(&self) -> bool { + self.verbose + } + + /// dryrun returns true if the dry-run command line option + /// was given, otherwise false is returned. + pub fn dryrun(&self) -> bool { + self.dryrun + } + + /// strict returns true if the strict flag was given, otherwise + /// false is returned. + pub fn strict(&self) -> bool { + self.strict + } +} diff --git a/rust/pvapconfig/src/config.rs b/rust/pvapconfig/src/config.rs new file mode 100644 index 00000000..28d2ac75 --- /dev/null +++ b/rust/pvapconfig/src/config.rs @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2023 +// +//! Functions around handling the pvapconfig configuration file +// + +use openssl::sha::sha256; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use serde_yaml::{self}; +use std::fs::File; +use std::slice::Iter; + +pub const STR_MODE_EP11: &str = "ep11"; +pub const STR_MODE_ACCEL: &str = "accel"; + +const RE_EP11_MKVP_32: &str = r"^(0x)?([[:xdigit:]]{32})$"; +const RE_EP11_MKVP_64: &str = r"^(0x)?([[:xdigit:]]{64})$"; +const RE_SERIALNR: &str = r"^(\S{16})$"; +const RE_EP11_GEN: &str = r"^cex(8)$"; +const RE_ACCEL_GEN: &str = r"^cex([4-8])$"; +const RE_SECRETID: &str = r"^(0x)?([[:xdigit:]]{64})$"; + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[serde(default, deny_unknown_fields)] +pub struct ApConfigEntry { + pub name: String, // name and description are unmodified from the config file + pub description: String, // accel after validation ep11 after validation + pub mode: String, // "accel" "ep11" + pub mkvp: String, // empty 32 hex lowercase characters + pub serialnr: String, // empty empty or 16 non-whitespace characters + pub mingen: String, // empty or "cex4"..."cex8" empty or "cex8" + pub secretid: String, // empty 64 hex lowercase characters +} + +impl ApConfigEntry { + fn validate_secretid(&mut self) -> Result<(), String> { + // either secret id or name may be given + if self.secretid.is_empty() && self.name.is_empty() { + return Err("Neither secretid nor name given.".to_string()); + } + // if name is given, calculate sha256 digest for this name + // test for the hash calculated here can be done with openssl: + // echo -n "Hello" >in.bin; openssl dgst -sha256 -binary -out out.bin in.bin; hexdump -C out.bin + if self.name.is_empty() { + return Ok(()); + } + let hash = sha256(self.name.as_bytes()); + let hashstr = crate::helper::u8_to_hexstring(&hash); + // if there is a secretid given, this must match to the hash + if !self.secretid.is_empty() { + if self.secretid != hashstr { + return Err("Mismatch between sha256(name) and secretid.".to_string()); + } + } else { + self.secretid = hashstr; + } + Ok(()) + } + + /// # Panics + /// Panics if the compilation of a static regular expression fails. + fn validate_ep11_entry(&mut self) -> Result<(), String> { + // mkvp is required + let mut mkvp = self.mkvp.trim().to_lowercase(); + if mkvp.is_empty() { + return Err("Mkvp value missing.".to_string()); + } + // either 64 hex or 32 hex + if Regex::new(RE_EP11_MKVP_64).unwrap().is_match(&mkvp) { + // need to cut away the last 32 hex characters + mkvp = String::from(&mkvp[..mkvp.len() - 32]) + } else if Regex::new(RE_EP11_MKVP_32).unwrap().is_match(&mkvp) { + // nothing to do here + } else { + return Err(format!("Mkvp value '{}' is not valid.", &self.mkvp)); + } + self.mkvp = match mkvp.strip_prefix("0x") { + Some(rest) => String::from(rest), + None => mkvp, + }; + // serialnr is optional + let serialnr = self.serialnr.trim().to_string(); + if !serialnr.is_empty() && !Regex::new(RE_SERIALNR).unwrap().is_match(&serialnr) { + return Err(format!("Serialnr value '{}' is not valid.", &self.serialnr)); + } + self.serialnr = serialnr; + // mingen is optional, but if given only CEX8 is valid + let mingen = self.mingen.trim().to_lowercase(); + if !mingen.is_empty() && !Regex::new(RE_EP11_GEN).unwrap().is_match(&mingen) { + return Err(format!("Mingen value '{}' is not valid.", &self.mingen)); + } + self.mingen = mingen; + // secretid or name is required + let secretid = self.secretid.trim().to_lowercase(); + if !secretid.is_empty() && !Regex::new(RE_SECRETID).unwrap().is_match(&secretid) { + return Err(format!("Secretid value '{}' is not valid.", &self.secretid)); + } + self.secretid = match secretid.strip_prefix("0x") { + Some(rest) => String::from(rest), + None => secretid, + }; + // name is optional, ignored here + // description is optional, ignored here + // but the secretid needs some more validation + self.validate_secretid() + } + + /// # Panics + /// Panics if the compilation of a static regular expression fails. + fn validate_accel_entry(&mut self) -> Result<(), String> { + // mkvp is ignored + self.mkvp.clear(); + // serialnr is ignored + self.serialnr.clear(); + // mingen is optional, but if given must match to CEX4..CEX8 + let mingen = self.mingen.trim().to_lowercase(); + if !mingen.is_empty() && !Regex::new(RE_ACCEL_GEN).unwrap().is_match(&mingen) { + return Err(format!("Mingen value '{}' is not valid.", &self.mingen)); + } + self.mingen = mingen; + // secretid is ignored + self.secretid.clear(); + // name is optional, ignored here + // description is optional, ignored here + Ok(()) + } + + fn validate(&mut self) -> Result<(), String> { + // trim name + self.name = self.name.trim().to_string(); + // mode is always required + let mode = self.mode.trim().to_lowercase(); + match mode.as_str() { + STR_MODE_EP11 => { + self.mode = mode; + self.validate_ep11_entry()?; + } + STR_MODE_ACCEL => { + self.mode = mode; + self.validate_accel_entry()?; + } + _ => return Err(format!("Unknown or invalid mode '{}'.", mode)), + } + Ok(()) + } +} + +/// Wrapper object around Vector of ApConfigEntry +pub struct ApConfigList(Vec); + +impl ApConfigList { + #[cfg(test)] // only used in test code + pub fn from_apconfigentry_vec(apconfigs: Vec) -> ApConfigList { + ApConfigList(apconfigs) + } + + pub fn iter(&self) -> Iter<'_, ApConfigEntry> { + self.0.iter() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn read_yaml_file(fname: &str) -> Result, String> { + let file = match File::open(fname) { + Ok(f) => f, + Err(err) => { + return Err(format!( + "Failure to open AP config file {}: {:?}", + fname, err + )) + } + }; + match serde_yaml::from_reader(file) { + Ok(cfg) => Ok(cfg), + Err(err) => Err(format!( + "Failure parsing AP config file {}: {:?}", + fname, err + )), + } + } + + fn validate(config: &mut [ApConfigEntry]) -> Result<(), String> { + for (i, entry) in config.iter_mut().enumerate() { + let ename = if !entry.name.trim().is_empty() { + format!("AP config entry {} '{}'", i, entry.name.trim()) + } else { + format!("AP config entry {}", i) + }; + if let Err(err) = &entry.validate() { + return Err(format!("{}: {}", ename, err)); + } + } + Ok(()) + } + + /// Read in and validate the yaml configuration from a file. + /// Returns a Result with Ok(ApConfigList) on success + /// or an Err(errorstring) on failure. + pub fn read_and_validate_yaml_file(fname: &str) -> Result { + let mut apconfig = ApConfigList::read_yaml_file(fname)?; + ApConfigList::validate(&mut apconfig)?; + Ok(ApConfigList(apconfig)) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use std::env; + use std::fs; + use std::io::Write; + + const GOOD_CONFIGS: [&str; 8] = [ + "# good test 1 +- name: my Accelerator + mode: AcCel + mingen: Cex7\n", + "# good test 2 +- name: my Accelerator 2 + description: Accelerator entry with description + mode: Accel\n", + "# good test 3 +- name: my EP11 APQN 1 + mode: Ep11 + mkvp: 0xDB3C3B3C3F097DD55EC7EB0E7FDBCB93 + serialnr: 93AADFK719460083 + secretid: 0xBC9d46c052BC3574454C5715757274629a283767ed237922cfb8651c0e77320A\n", + "# good test 4 +- name: my EP11 APQN 2 + mode: EP11 + mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93 + serialnr: 93aaDHzu42082261 + secretid: 0x2ca853f959fc5ce5f1888cb48dae39514a27bb66520ac85f6073a7f678d262c0\n", + "# good test 5 +- name: my EP11 APQN 3 + mode: EP11 + mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93db3c3b3c3f097dd55ec7eb0e7fdbcb93 + serialnr: 93aaDHzu42082261 + secretid: 0xd146c9ae77cdff25fa87a5b3487587dc29a4e391b315c98570e8fa2e2ec91454\n", + "# no name but secretid given +- mode: EP11 + mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93 + secretid: 0x0767668dd22f23fa675c4641e04bb4e991f443be4df13ce3896b8eeca59fcc10\n", + "# no secretid but name given +- mode: EP11 + name: My-EP11-AP-config + mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93\n", + "# secretid and name given +- mode: EP11 + name: My-EP11-AP-config + mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93 + secretid: 0x0767668dd22f23fa675c4641e04bb4e991f443be4df13ce3896b8eeca59fcc10\n", + ]; + + const BAD_CONFIGS: [&str; 12] = [ + "# mode missing +- name: bad test 1 + mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93 + secretid: 0x0767668dd22f23fa675c4641e04bb4e991f443be4df13ce3896b8eeca59fcc10\n", + "# invalid mode +- name: bad test 2 + mode: CCA + mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93 + secretid: 0x0767668dd22f23fa675c4641e04bb4e991f443be4df13ce3896b8eeca59fcc10\n", + "# Accelerator with wrong CEX3 +- name: bad test 3 + mode: Accel + mingen: Cex3\n", + "# Accelerator with wrong CEX9 +- name: bad test 4 + mode: Accel + mingen: CEX9\n", + "# EP11 with mkvp missing +- name: bad test 5 + mode: EP11 + serialnr: 93AADHZU42082261\n", + "# EP11 with non hex mkvp +- name: bad test 6 + mode: EP11 + mkvp: 0xabcdefghijklmnopqqponmlkjihgfedcba + serialnr: 93AADHZU42082261\n", + "# EP11 with mkvp too big +- name: bad test 7 + mode: EP11 + mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93aa + serialnr: 93AADHZU42082261\n", + "# EP11 with mkvp too small +- name: bad test 8 + mode: EP11 + mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb + serialnr: 93AADHZU42082261\n", + "# EP11 with invalid CEXx +- name: bad test 9 + mode: EP11 + mingen: CEX7 + mkvp: 0x00112233445566778899aabbccddeeff + serialnr: 93AADHZU42082261\n", + "# EP11 with invalid Serialnr +- name: bad test 10 + mode: EP11 + mkvp: 0x00112233445566778899aabbccddeeff + serialnr: 93AADHZU4208226\n", + "# EP11 with invalid Serialnr +- name: bad test 11 + mode: EP11 + mkvp: 0x00112233445566778899aabbccddeeff + serialnr: 93AAD ZU42082261\n", + "# EP11 with sha256(name) != secretid +- name: bad test 12 + mode: EP11 + mkvp: 0x00112233445566778899aabbccddeeff + serialnr: AABBCCDDEEFFGGHH + secretid: 0x2ca853f959fc5ce5f1888cb48dae39514a27bb66520ac85f6073a7f678d262c0\n", + ]; + + const BAD_DESERIALIZE: [&str; 2] = [ + "/*\ntotal nonsense\n */\n", + "# wrong/unknown field +- name: de-serialize failure 1 + type: EP11\n", + ]; + + fn write_yaml_config_to_temp_file(content: &str) -> Result { + let dir = env::temp_dir(); + let rnd = rand::random::(); + let fname = format!("{}/config-test-{}.yaml", dir.to_str().unwrap(), rnd); + let mut f = match File::create(&fname) { + Ok(f) => f, + Err(_) => return Err(format!("Failure creating temp file '{fname}'.")), + }; + match f.write_all(content.as_bytes()) { + Ok(_) => Ok(fname), + Err(_) => { + fs::remove_file(&fname).ok(); + Err(format!("Failure writing to temp file '{fname}'.")) + } + } + } + + #[test] + fn test_good_yaml() { + for yaml in GOOD_CONFIGS { + let f = write_yaml_config_to_temp_file(yaml).unwrap(); + let config = ApConfigList::read_and_validate_yaml_file(&f).unwrap(); + assert!(config.len() > 0); + fs::remove_file(&f).ok(); + } + } + #[test] + fn test_bad_yaml() { + for yaml in BAD_CONFIGS { + let f = write_yaml_config_to_temp_file(yaml).unwrap(); + let r = ApConfigList::read_and_validate_yaml_file(&f); + assert!(r.is_err()); + fs::remove_file(&f).ok(); + } + } + #[test] + fn test_invalid_deserizalize() { + for yaml in BAD_DESERIALIZE { + let f = write_yaml_config_to_temp_file(yaml).unwrap(); + let r = ApConfigList::read_and_validate_yaml_file(&f); + assert!(r.is_err()); + fs::remove_file(&f).ok(); + } + } + #[test] + fn test_sha256() { + assert!( + crate::helper::u8_to_hexstring(&sha256("Hello".as_bytes())) + == "185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969" + ); + assert!( + crate::helper::u8_to_hexstring(&sha256("SECRET1".as_bytes())) + == "03153249db7ce46b0330ffb1a760b59710531af08ec4d7f8424a6870fae49360" + ); + assert!( + crate::helper::u8_to_hexstring(&sha256("SECRET2".as_bytes())) + == "258499e710e0bd3bb878d6bac7e478b30f3f3e72566989f638c4143d14f6c0b6" + ); + } +} diff --git a/rust/pvapconfig/src/helper.rs b/rust/pvapconfig/src/helper.rs new file mode 100644 index 00000000..c4670cef --- /dev/null +++ b/rust/pvapconfig/src/helper.rs @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2023 +// +//! Collection of helper functions for pvapconfig +// + +use regex::Regex; +use std::error::Error; +use std::fs; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::PathBuf; + +pub const PATH_PVAPCONFIG_LOCK: &str = "/run/lock/pvapconfig.lock"; + +/// Convert u8 slice to (lowercase) hex string +pub fn u8_to_hexstring(slice: &[u8]) -> String { + let s = String::with_capacity(2 * slice.len()); + slice.iter().fold(s, |acc, e| acc + &format!("{e:02x}")) +} + +/// Convert hexstring to u8 vector +/// The hexstring may contain whitespaces which are ignored. +/// If there are other characters in there or if the number +/// of hex characters is uneven panic() is called. +/// # Panics +/// Panics if the given string contains characters other than +/// hex digits and whitespace. Panics if the number of hex digits +/// is not even. +#[cfg(test)] // currently only used in test code +pub fn hexstring_to_u8(hex: &str) -> Vec { + let mut s = String::new(); + for c in hex.chars() { + if c.is_ascii_hexdigit() { + s.push(c); + } else if c.is_whitespace() { + // ignore + } else { + panic!("Invalid character '{c}'"); + } + } + if s.len() % 2 == 1 { + panic!("Uneven # of hex characters in '{s}'"); + } + let mut hex_bytes = s.as_bytes().iter().map_while(|b| match b { + b'0'..=b'9' => Some(b - b'0'), + b'a'..=b'f' => Some(b - b'a' + 10), + b'A'..=b'F' => Some(b - b'A' + 10), + _ => None, + }); + let mut bytes = Vec::with_capacity(s.len()); + while let (Some(h), Some(l)) = (hex_bytes.next(), hex_bytes.next()) { + bytes.push(h << 4 | l) + } + bytes +} + +/// Read sysfs file into string +pub fn sysfs_read_string(fname: &str) -> Result> { + let mut file = File::open(fname)?; + let mut content = String::new(); + file.read_to_string(&mut content)?; + let trimmed_content = String::from(content.trim()); + Ok(trimmed_content) +} + +/// Write string into sysfs file +pub fn sysfs_write_string(fname: &str, value: &str) -> Result<(), Box> { + let mut file = OpenOptions::new().write(true).open(fname)?; + file.write_all(value.as_bytes())?; + Ok(()) +} + +/// Read sysfs file content and parse as i32 value +pub fn sysfs_read_i32(fname: &str) -> Result> { + let content = sysfs_read_string(fname)?; + Ok(content.parse::()?) +} + +/// Write an i32 value into a sysfs file +pub fn sysfs_write_i32(fname: &str, value: i32) -> Result<(), Box> { + return sysfs_write_string(fname, &value.to_string()); +} + +/// For a given (sysfs) directory construct a list of all subdirs +/// and give it back as a vector of strings. If there is no subdir, +/// the vector is empty. +pub fn sysfs_get_list_of_subdirs(dname: &str) -> Result, Box> { + let mut v: Vec = Vec::new(); + let entries = fs::read_dir(dname)?; + for entry in entries.flatten() { + let file_type = match entry.file_type() { + Ok(ft) => ft, + _ => continue, + }; + if !file_type.is_dir() { + continue; + } + let fname = match entry.file_name().into_string() { + Ok(s) => s, + _ => continue, + }; + v.push(fname); + } + Ok(v) +} + +/// For a given (sysfs) directory construct a list of all subdirs which +/// match to the given regular expression and give the list back as a +/// vector of strings. If there is no subdir, the vector is empty. +pub fn sysfs_get_list_of_subdirs_matching_regex( + dname: &str, + regex: &str, +) -> Result, Box> { + let mut v: Vec = Vec::new(); + let re = Regex::new(regex)?; + let entries = sysfs_get_list_of_subdirs(dname)?; + for entry in entries { + if re.is_match(&entry) { + v.push(entry); + } + } + Ok(v) +} + +/// LockFile for inter-process locking +/// +/// Simple class for process locking for pvapconfig. +/// The lock concept is simple: The existence of a file is used as the +/// locking indicator. If the file exists something is locked, if it does +/// not exist something is not locked. In the lock file the PID of the +/// process created the file ("owning this file") is written in. +/// With the ProcessLock object leaving scope the associated lock file +/// is automatically deleted. It is assumed that the creation of a file +/// is an atomic operation - that's true for most filesystems but may +/// cause problems with network based filesystems. +/// Example: +/// ``` +/// let lock = LockFile::lock("/var/lock/process.lock"); +/// assert!(lock.is_ok()); +/// let lock2 = LockFile::lock("/var/lock/process.lock"); +/// assert!(lock2.is_err()); +/// drop(lock); +/// let lock3 = LockFile::lock("/var/lock/process.lock"); +/// assert!(lock3.is_ok()); +/// ``` +#[derive(Debug)] +pub struct LockFile { + lockfile: PathBuf, +} + +impl LockFile { + /// Try to establish the lock file. + /// Upon success the given file is fresh created and has the pid of this + /// process written in. The function returns a new LockFile object + /// which has implemented the Drop Trait. So with this object going out + /// of scope the lock file is deleted. If establishing the lock file + /// fails for any reason (for example the file already exists), the + /// function fails with returning an Error string. This function does + /// NOT panic if establishing the lock file fails for any reason. If + /// the lock file could be esablished but writing in the PID fails, a + /// warning is printed but the function continues with returning a + /// LockFile object. + pub fn try_lock(fname: &str) -> Result { + let lockfile = PathBuf::from(fname); + let mut file = match OpenOptions::new() + .write(true) + .create_new(true) + .open(&lockfile) + { + Err(err) => { + return Err(format!( + "Failure trying to create lock file {fname}: {err:?}." + )) + } + Ok(f) => f, + }; + let _ = file + .write(format!("{}", std::process::id()).as_bytes()) + .map_err(|err| { + println!("Warning: could not write PID into lockfile {fname}: {err:?}.") + }); + Ok(LockFile { lockfile }) + } +} + +impl Drop for LockFile { + fn drop(&mut self) { + let _ = fs::remove_file(&self.lockfile).map_err(|err| { + println!( + "Warning: could not remove lockfile {}: {err:?}.", + self.lockfile.display() + ) + }); + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + // Only very simple tests + + const TEST_BYTES: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]; + const TEST_HEXSTR: &str = "0123456789abcdef"; + + #[test] + fn test_u8_to_hexstring() { + let str = u8_to_hexstring(&TEST_BYTES); + assert!(str == TEST_HEXSTR); + } + #[test] + fn test_hexstring_to_u8() { + let bytes = hexstring_to_u8(TEST_HEXSTR); + assert!(bytes.as_slice() == TEST_BYTES); + } + #[test] + fn test_sysfs_read_string() { + let r = sysfs_read_string("/proc/cpuinfo"); + assert!(r.is_ok()); + } + #[test] + fn test_sysfs_read_i32() { + let r = sysfs_read_i32("/proc/sys/kernel/random/entropy_avail"); + assert!(r.is_ok()); + } + #[test] + fn test_sysfs_get_list_of_subdirs() { + let r = sysfs_get_list_of_subdirs("/proc/self"); + assert!(r.is_ok()); + let v = r.unwrap(); + assert!(!v.is_empty()); + } + #[test] + fn test_sysfs_get_list_of_subdirs_matching_regex() { + let r = sysfs_get_list_of_subdirs_matching_regex("/proc/self", "fd.*"); + assert!(r.is_ok()); + let v = r.unwrap(); + assert!(!v.is_empty()); + for e in v { + assert!(e.strip_prefix("fd").is_some()); + } + } + #[test] + fn test_sysfs_write_i32() { + const TEST_PATH: &str = "/tmp/test_sysfs_write_i32"; + let mut file = File::create(TEST_PATH).unwrap(); + let _ = file.write_all(b"XYZ"); + drop(file); + let r = sysfs_read_i32(TEST_PATH); + assert!(r.is_err()); + let r = sysfs_write_i32(TEST_PATH, 999); + assert!(r.is_ok()); + let r = sysfs_read_i32(TEST_PATH); + assert!(r.is_ok()); + let v = r.unwrap(); + assert!(v == 999); + let _ = fs::remove_file(TEST_PATH); + } + #[test] + fn test_lockfile() { + let r1 = LockFile::try_lock(PATH_PVAPCONFIG_LOCK); + assert!(r1.is_ok()); + let r2 = LockFile::try_lock(PATH_PVAPCONFIG_LOCK); + assert!(r2.is_err()); + drop(r1); + let r3 = LockFile::try_lock(PATH_PVAPCONFIG_LOCK); + assert!(r3.is_ok()); + } +} diff --git a/rust/pvapconfig/src/main.rs b/rust/pvapconfig/src/main.rs new file mode 100644 index 00000000..07899cd9 --- /dev/null +++ b/rust/pvapconfig/src/main.rs @@ -0,0 +1,668 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2023 +// +//! pvapconfig - Tool to automatically set up the AP configuration +//! within an IBM Secure Execution guest. +// + +mod ap; +mod cli; +mod config; +mod helper; +mod uv; + +use ap::{Apqn, ApqnList}; +use cli::ARGS; +use config::{ApConfigEntry, ApConfigList}; +use helper::{LockFile, PATH_PVAPCONFIG_LOCK}; +use pv::uv::{ListableSecretType, SecretList}; +use std::process::ExitCode; +use utils::release_string; + +/// Simple macro for +/// if Cli::verbose() { +/// print!(...); +/// } +macro_rules! info { + ($($arg:tt)*) => {{ + if ARGS.verbose() { + print!($($arg)*); + } + }}; +} + +/// Simple macro for the main function only +/// Does a eprintln of the arguments and then +/// return with exit failure. +macro_rules! println_and_exit_failure { + ($($arg:tt)*) => {{ + eprintln!($($arg)*); + return ExitCode::FAILURE; + }}; +} + +/// Simple macro for the main function only +/// Check if given object has is_err() true and +/// then eprintln the unwrapped error and +/// returns with exit failure. +macro_rules! on_error_print_and_exit { + ($r:expr) => { + if $r.is_err() { + eprintln!("{}", $r.unwrap_err()); + return ExitCode::FAILURE; + } + }; +} + +fn main() -> ExitCode { + // handle version option + if cli::ARGS.version { + println!( + "{} version {}\nCopyright IBM Corp. 2023", + env!("CARGO_PKG_NAME"), + release_string!() + ); + return ExitCode::SUCCESS; + } + + // make sure only one pvapconfig instance is running + let r = LockFile::try_lock(PATH_PVAPCONFIG_LOCK); + on_error_print_and_exit!(r); + let _lockfile = r.unwrap(); + + // AP bus check + info!("Checking AP bus support and facilities...\n"); + let r = ap::check_ap_bus_support(); + on_error_print_and_exit!(r); + let r = ap::ap_bus_has_apsb_support(); + on_error_print_and_exit!(r); + info!("AP bus support and facilities are ok.\n"); + + // UV check + info!("Checking UV support and environment...\n"); + if !pv::misc::pv_guest_bit_set() { + println_and_exit_failure!("Failure: this is not a SE guest."); + } + let r = uv::has_list_secrets_facility(); + on_error_print_and_exit!(r); + info!("UV support and environment is ok.\n"); + + // read configuration + let configfile: &str = match &cli::ARGS.config { + Some(f) => f, + _ => cli::PATH_DEFAULT_CONFIG_FILE, + }; + info!( + "Reading AP configuration entries from file '{}'...\n", + configfile + ); + let apconfig: ApConfigList = match ApConfigList::read_and_validate_yaml_file(configfile) { + Ok(apcfg) => apcfg, + Err(err) => println_and_exit_failure!("{}", err), + }; + if apconfig.is_empty() { + println!( + "No AP configuration entries in config file '{}': Nothing to do.", + configfile + ); + return ExitCode::SUCCESS; + } + info!("Found {} AP configuration entries.\n", apconfig.len()); + + // get list of secrets from UV + info!("Fetching list of secrets from UV...\n"); + let secrets: SecretList = match uv::gather_secrets() { + Err(e) => println_and_exit_failure!("{}", e), + Ok(los) => los, + }; + info!("Fetched {} Secret entries from UV.\n", secrets.len()); + + // Warning if no UV secrets given but AP config entries require it + let non_accel_apc = apconfig + .iter() + .filter(|apc| apc.mode != config::STR_MODE_ACCEL) + .count(); + if non_accel_apc > 0 && secrets.is_empty() { + println!( + "Warning: No UV Secrets given but at least one AP config entry requires a Secret." + ); + } + + info!("Waiting for AP bus bindings complete...\n"); + if !ap::wait_for_ap_bus_bindings_complete() { + return ExitCode::FAILURE; + } + info!("Fetching list of available APQNs...\n"); + let mut apqns: ApqnList = match ApqnList::gather_apqns() { + Some(l) => l, + None => return ExitCode::FAILURE, + }; + if apqns.is_empty() { + info!("List of available APQNs is empty: So there's nothing to do.\n"); + return ExitCode::SUCCESS; + } + info!("Found {} APQNs.\n", apqns.len()); + // check MK restriction + if !apqns.check_mk_restriction() { + return ExitCode::FAILURE; + } + + // now the real work + info!("Applying AP configuration...\n"); + let n = match do_ap_config(&mut apqns, &secrets, &apconfig, false) { + Err(e) => println_and_exit_failure!("{}", e), + Ok(n) => n, + }; + + if n == 0 { + println_and_exit_failure!( + "None out of {} AP config entries could be applied.", + apconfig.len() + ); + } else if ARGS.strict() && n != apconfig.len() { + println_and_exit_failure!( + "Strict flag given and only {} out of {} AP config entries have been applied.", + n, + apconfig.len() + ); + } + + info!( + "Successfully applied {} out of {} AP config entries.\n", + n, + apconfig.len() + ); + + ExitCode::SUCCESS +} + +/// The real worker function +/// +/// This is the real algorithm which is trying to apply the +/// AP configuration read from the config file to the existing +/// APQNs with the info from the list of secrets from the UV. +/// Returns the nr of AP config entries which are fullfilled +/// after the function ended. +/// apqns needs to be mutable as the function does a resort +/// but content stays the same. +fn do_ap_config( + apqns: &mut ApqnList, + secrets: &SecretList, + apconfig: &ApConfigList, + fntest: bool, +) -> Result { + let mut resolved_entries = 0; + let mut apconfig_done = vec![false; apconfig.len()]; + let mut apqn_done = vec![false; apqns.len()]; + + // Preparation: Sort APQNs by generation. + // All the following steps iterate through the list + // of APQNs. So by sorting the APQNs starting with + // highest card generation down to the older card + // generations we prefer newer card generations over + // older card generations. + apqns.sort_by_gen(); + + // Step 1: + // Go through all AP config entries and try to find an APQN + // which already matches to this entry. If such an APQN is + // found mark the AP config entry as done, and mark the APQN + // as used so that entry and APQN will get skipped over in + // the next steps. + + for (ci, apc) in apconfig.iter().enumerate() { + let cistr = if !apc.name.is_empty() { + format!("#{} '{}'", ci + 1, apc.name) + } else { + format!("#{}", ci + 1) + }; + for (ai, apqn) in apqns.iter().enumerate() { + if apqn_done[ai] { + continue; + } + if !config_and_apqn_match(apc, apqn) { + continue; + } + if fntest { + continue; + } + match apqn.mode { + ap::ApqnMode::Accel => { + // check bind state of this APQN + let bind_state_ok = match apqn.bind_state() { + Err(err) => { + eprintln!("Warning: Failure reading APQN {apqn} bind state: {err}"); + false + } + Ok(ap::BindState::Bound) => true, + Ok(_) => false, + }; + if !bind_state_ok { + continue; + } + // This APQN matches to the current AP config entry and is already bound. + // So this AP config entry is satisfied: mark this config enty as done + // and mark this APQN as used. + info!("Accelerator APQN {apqn} already satisfies AP config entry {cistr}.\n"); + apconfig_done[ci] = true; + apqn_done[ai] = true; + resolved_entries += 1; + break; + } + ap::ApqnMode::Ep11 => { + // check association state of this APQN + let (assoc_state_ok, assoc_idx) = match apqn.associate_state() { + Err(err) => { + eprintln!( + "Warning: Failure reading APQN {apqn} associate state: {err}" + ); + (false, 0) + } + Ok(ap::AssocState::Associated(idx)) => (true, idx), + Ok(_) => (false, 0), + }; + if !assoc_state_ok { + continue; + } + // check association index + let r = secrets.iter().find(|&se| { + se.stype() == ListableSecretType::Association + && se.id().len() == uv::AP_ASSOC_SECRET_ID_SIZE + && se.index() == assoc_idx + && helper::u8_to_hexstring(se.id()) == apc.secretid + }); + if r.is_none() { + continue; + } + // This APQN matches to the current AP config entry and is already + // associated with the right secret id. So this AP config entry is + // satisfied: mark this config enty as done and mark this APQN as used. + info!("EP11 APQN {apqn} already satisfies AP config entry {cistr}.\n"); + apconfig_done[ci] = true; + apqn_done[ai] = true; + resolved_entries += 1; + break; + } + _ => { + // (currently) unknown/unsupported APQN mode + } + } + } + } + + // Step 2: + // All APQNs NOT marked as done are now examined for their bind + // and association state and maybe reset to "unbound". + + for (ai, apqn) in apqns.iter().enumerate() { + if apqn_done[ai] || fntest { + continue; + } + match apqn.bind_state() { + Err(err) => eprintln!("Warning: Failure reading APQN {apqn} bind state: {err}"), + Ok(ap::BindState::Bound) => { + info!("Unbind APQN {apqn} as this bind/associate does not match to any AP config entry.\n"); + if !ARGS.dryrun() { + if let Err(err) = apqn.set_bind_state(ap::BindState::Unbound) { + return Err(format!("Failure unbinding APQN {apqn}: {err}")); + } + } + } + Ok(_) => {} + }; + } + + // Step 3: + // Go through all remaining AP config entries and try to fullfill each + // by searching for an APQN which would match to this config entry and + // then prepare this APQN (bind, maybe associate). + for (ci, apc) in apconfig.iter().enumerate() { + let cistr = if !apc.name.is_empty() { + format!("#{} '{}'", ci + 1, apc.name) + } else { + format!("#{}", ci + 1) + }; + if apconfig_done[ci] { + continue; + } + for (ai, apqn) in apqns.iter().enumerate() { + if apqn_done[ai] { + continue; + } + if !config_and_apqn_match(apc, apqn) { + continue; + } + match apqn.mode { + ap::ApqnMode::Accel => { + // try to bind this accelerator APQN + if ARGS.verbose() || fntest { + println!("Bind APQN {apqn} to match to AP config entry {cistr}."); + } + if !(ARGS.dryrun() || fntest) { + if let Err(err) = apqn.set_bind_state(ap::BindState::Bound) { + // bind failed, unbind/reset this apqn, return with failure + let _ = apqn.set_bind_state(ap::BindState::Unbound); + return Err(format!("Failure binding APQN {apqn}: {err}")); + } + } + apconfig_done[ci] = true; + apqn_done[ai] = true; + resolved_entries += 1; + break; + } + ap::ApqnMode::Ep11 => { + // EP11 needs bind and associate, but before doing this let's + // check out which secret index to use with the associate + let se = match secrets.iter().find(|&se| { + se.stype() == ListableSecretType::Association + && se.id().len() == uv::AP_ASSOC_SECRET_ID_SIZE + && helper::u8_to_hexstring(se.id()) == apc.secretid + }) { + None => { + eprintln!("Warning: Secret id '{}' from config entry {} not found in UV secrets list.", + apc.secretid, cistr); + break; + } + Some(se) => se, + }; + // try to bind + if ARGS.verbose() || fntest { + println!( + "Bind APQN {apqn} to match to AP config entry {cistr} (step 1/2)." + ); + } + if !(ARGS.dryrun() || fntest) { + if let Err(err) = apqn.set_bind_state(ap::BindState::Bound) { + // bind failed, unbind/reset this apqn, return with failure + let _ = apqn.set_bind_state(ap::BindState::Unbound); + return Err(format!("Failure binding APQN {}: {}", apqn, err)); + } + } + // try to associate + if ARGS.verbose() || fntest { + println!( + "Associate APQN {} with uv secrets index {} to match AP config entry {} (step 2/2).", + apqn, se.index(), cistr + ); + } + if !(ARGS.dryrun() || fntest) { + let apas = ap::AssocState::Associated(se.index()); + apqn.set_associate_state(apas) + .map_err(|err| format!("Failure associating APQN {apqn}: {err}"))?; + } + apconfig_done[ci] = true; + apqn_done[ai] = true; + resolved_entries += 1; + break; + } + _ => { + // (currently) unknown/unsupported APQN mode + } + } + } + } + + Ok(resolved_entries) +} + +/// # Panics +/// Panics if mingen for an accelerator has not a number as the 4th character. +/// Panics if mingen for an ep11 has not a number as the 4th character. +/// Please note this can not happen, as mingen is already checked via RE +/// during storing the value into mingen. +fn config_and_apqn_match(apc: &ApConfigEntry, apqn: &Apqn) -> bool { + if apc.mode == config::STR_MODE_ACCEL && apqn.mode == ap::ApqnMode::Accel { + // config and apqn are accelerators + // maybe check mingen + if !apc.mingen.is_empty() { + let mingen = &apc.mingen[3..].parse::().unwrap(); + if mingen < &apqn.gen { + return false; + } + } + return true; + } else if apc.mode == config::STR_MODE_EP11 && apqn.mode == ap::ApqnMode::Ep11 { + // config and apqn are ep11 + let info = match &apqn.info { + Some(ap::ApqnInfo::Ep11(i)) => i, + _ => return false, + }; + // maybe check mingen + if !apc.mingen.is_empty() { + let mingen = &apc.mingen[3..].parse::().unwrap(); + if mingen < &apqn.gen { + return false; + } + } + // maybe check serialnr + if !apc.serialnr.is_empty() && apc.serialnr != info.serialnr { + return false; + } + // check mkvp, currently an ep11 config entry must state an mkvp value + // whereas an ep11 info from an APQN may have an empty mkvp value to + // indicate that there is no WK set on this APQN. + if apc.mkvp != info.mkvp { + return false; + } + return true; + } + false +} + +#[cfg(test)] +mod tests { + + use super::*; + use helper::hexstring_to_u8; + use pv::uv::SecretEntry; + + // This is more or less only a test for the do_ap_config() function + // However, this is THE main functionality of the whole application. + + fn make_test_apqns() -> Vec { + let mut v = Vec::new(); + v.push(ap::Apqn { + name: String::from("10.0007"), + card: 16, + domain: 7, + gen: 8, + mode: ap::ApqnMode::Accel, + info: Option::Some(ap::ApqnInfo::Accel(ap::ApqnInfoAccel {})), + }); + v.push(ap::Apqn { + name: String::from("11.0008"), + card: 17, + domain: 8, + gen: 8, + mode: ap::ApqnMode::Ep11, + info: Option::Some(ap::ApqnInfo::Ep11(ap::ApqnInfoEp11 { + serialnr: String::from("93AADFK719460083"), + mkvp: String::from("db3c3b3c3f097dd55ec7eb0e7fdbcb93"), + })), + }); + v.push(ap::Apqn { + name: String::from("12.0009"), + card: 18, + domain: 9, + gen: 8, + mode: ap::ApqnMode::Ep11, + info: Option::Some(ap::ApqnInfo::Ep11(ap::ApqnInfoEp11 { + serialnr: String::from("93AADHZU42082261"), + mkvp: String::from("4a27bb66520ac85f6073a7f678d262c0"), + })), + }); + v.push(ap::Apqn { + name: String::from("12.000a"), + card: 18, + domain: 10, + gen: 8, + mode: ap::ApqnMode::Ep11, + info: Option::Some(ap::ApqnInfo::Ep11(ap::ApqnInfoEp11 { + serialnr: String::from("93AADHZU42082261"), + mkvp: String::from("383d2a9ab781f35343554c5b3d9337cd"), + })), + }); + v.push(ap::Apqn { + name: String::from("13.000d"), + card: 19, + domain: 13, + gen: 8, + mode: ap::ApqnMode::Ep11, + info: Option::Some(ap::ApqnInfo::Ep11(ap::ApqnInfoEp11 { + serialnr: String::from("87HU397G150TZGR"), + mkvp: String::new(), + })), + }); + v.push(ap::Apqn { + name: String::from("13.000f"), + card: 19, + domain: 15, + gen: 8, + mode: ap::ApqnMode::Ep11, + info: Option::None, + }); + return v; + } + + fn make_assoc_secretentry(idx: u16, hexidstr: &str) -> SecretEntry { + let id = hexstring_to_u8(hexidstr); + let idlen: u32 = id.len().try_into().unwrap(); + let idarray = <&[u8; 32]>::try_from(id.as_slice()).unwrap(); + SecretEntry::new(idx, ListableSecretType::Association, *idarray, idlen) + } + + fn make_test_secrets() -> Vec { + let mut v: Vec = Vec::new(); + v.push(make_assoc_secretentry( + 33, + "3333333333333333333333333333333333333333333333333333333333333333", + )); + v.push(make_assoc_secretentry( + 13, + "bc9d46c052bc3574454c5715757274629a283767ed237922cfb8651c0e77320a", + )); + v.push(make_assoc_secretentry( + 44, + "4444444444444444444444444444444444444444444444444444444444444444", + )); + v.push(make_assoc_secretentry( + 15, + "06cdbbac76a595b481110d108154bc05ebbf900a0f16e36a24045998934fb1e9", + )); + v.push(make_assoc_secretentry( + 17, + "6831af07f8c8e7309a3ace9f3b5554d34e3eaa4a27a08fdee469e367c3fa3e9e", + )); + return v; + } + + fn make_test_apconfigs() -> Vec { + let mut v: Vec = Vec::new(); + v.push(config::ApConfigEntry { + name: String::from("test_1"), + description: String::from("test_1"), + mode: String::from("accel"), + mkvp: String::from(""), + serialnr: String::from(""), + mingen: String::from("cex8"), + secretid: String::from(""), + }); + v.push(config::ApConfigEntry { + name: String::from("test_2"), + description: String::from("test_2"), + mode: String::from("ep11"), + mkvp: String::from("db3c3b3c3f097dd55ec7eb0e7fdbcb93"), + serialnr: String::from("93AADFK719460083"), + mingen: String::from("cex8"), + secretid: String::from( + "bc9d46c052bc3574454c5715757274629a283767ed237922cfb8651c0e77320a", + ), + }); + v.push(config::ApConfigEntry { + name: String::from("test_3"), + description: String::from("test_3"), + mode: String::from("ep11"), + mkvp: String::from("4a27bb66520ac85f6073a7f678d262c0"), + serialnr: String::from(""), + mingen: String::from("cex8"), + secretid: String::from( + "06cdbbac76a595b481110d108154bc05ebbf900a0f16e36a24045998934fb1e9", + ), + }); + v.push(config::ApConfigEntry { + name: String::from("test_4"), + description: String::from("test_4"), + mode: String::from("ep11"), + mkvp: String::from("8be1eaf5c44e2fa8b18804551b604b1b"), + serialnr: String::from(""), + mingen: String::from("cex8"), + secretid: String::from( + "6831af07f8c8e7309a3ace9f3b5554d34e3eaa4a27a08fdee469e367c3fa3e9e", + ), + }); + return v; + } + + #[test] + fn test_do_ap_config_invocation_1() { + let test_apqns = make_test_apqns(); + let mut apqns: Vec = Vec::new(); + apqns.push(test_apqns[0].clone()); + let secrets: Vec = Vec::new(); + let secretlist = SecretList::new(secrets.len() as u16, secrets); + let test_apconfigs = make_test_apconfigs(); + let mut apconfig: Vec = Vec::new(); + apconfig.push(test_apconfigs[0].clone()); + let apcfglist = ApConfigList::from_apconfigentry_vec(apconfig); + let mut apqnlist = ApqnList::from_apqn_vec(apqns); + let r = do_ap_config(&mut apqnlist, &secretlist, &apcfglist, true); + assert!(r.is_ok()); + let n = r.unwrap(); + assert!(n == 1); + } + + #[test] + fn test_do_ap_config_invocation_2() { + let test_apqns = make_test_apqns(); + let mut apqns: Vec = Vec::new(); + apqns.push(test_apqns[1].clone()); + let mut secrets = make_test_secrets(); + while secrets.len() > 2 { + secrets.pop(); + } + let secretlist = SecretList::new(secrets.len() as u16, secrets); + let test_apconfigs = make_test_apconfigs(); + let mut apconfig: Vec = Vec::new(); + apconfig.push(test_apconfigs[1].clone()); + let apcfglist = ApConfigList::from_apconfigentry_vec(apconfig); + let mut apqnlist = ApqnList::from_apqn_vec(apqns); + let r = do_ap_config(&mut apqnlist, &secretlist, &apcfglist, true); + assert!(r.is_ok()); + let n = r.unwrap(); + assert!(n == 1); + } + + #[test] + fn test_do_ap_config_invocation_3() { + let test_apqns = make_test_apqns(); + let mut apqns: Vec = Vec::new(); + for a in test_apqns.iter() { + apqns.push(a.clone()); + } + apqns.reverse(); + let secrets = make_test_secrets(); + let secretlist = SecretList::new(secrets.len() as u16, secrets); + let test_apconfigs = make_test_apconfigs(); + let mut apconfig: Vec = Vec::new(); + for c in test_apconfigs.iter() { + apconfig.push(c.clone()); + } + let apcfglist = ApConfigList::from_apconfigentry_vec(apconfig); + let mut apqnlist = ApqnList::from_apqn_vec(apqns); + let r = do_ap_config(&mut apqnlist, &secretlist, &apcfglist, true); + assert!(r.is_ok()); + let n = r.unwrap(); + assert!(n == 3, "n = {} != 3", n); + } +} diff --git a/rust/pvapconfig/src/uv.rs b/rust/pvapconfig/src/uv.rs new file mode 100644 index 00000000..2f98bd5e --- /dev/null +++ b/rust/pvapconfig/src/uv.rs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2023 +// +//! UV related functions for pvapconfig +// + +use pv::uv::{ListCmd, SecretList, UvDevice, UvcSuccess}; +use regex::Regex; +use std::path::Path; + +/// The byte size of association secret of type 2 in struct SecretEntry +pub const AP_ASSOC_SECRET_ID_SIZE: usize = 32; + +const PATH_SYS_FW_UV_FACILITIES: &str = "/sys/firmware/uv/query/facilities"; + +const RE_UV_FACILITIES: &str = r"^(0x)?([[:xdigit:]]+)"; +const RE_UV_FAC_BIT_LIST_SECRETS: u32 = 30; + +/// Check UV facilities to offer the 'list secrets' call. +/// Returns a Result with Ok(()) if the 'list secrets' feature +/// is available, otherwise an Err(reasonstring) is returned where +/// the string denotes a hint which can be displayed. +/// # Panics +/// Panics if the compilation of a static regular expression fails. +/// Panics if RE_UV_FACILITIES does not match. +pub fn has_list_secrets_facility() -> Result<(), String> { + if !Path::new(PATH_SYS_FW_UV_FACILITIES).is_file() { + return Err(format!( + "UV facilities sysfs attribute not found (file {} does not exist).", + PATH_SYS_FW_UV_FACILITIES + )); + } + let facstr = match crate::helper::sysfs_read_string(PATH_SYS_FW_UV_FACILITIES) { + Ok(s) => s, + Err(err) => { + return Err(format!( + "Failure reading UV facilities from {PATH_SYS_FW_UV_FACILITIES} ({:?}).", + err + )) + } + }; + let re_uv_facilities = Regex::new(RE_UV_FACILITIES).unwrap(); + if !re_uv_facilities.is_match(&facstr) { + Err(format!("Failure parsing UV facilities entry '{facstr}'.")) + } else { + let caps = re_uv_facilities.captures(&facstr).unwrap(); + let fachex = caps.get(2).unwrap().as_str(); + let i: usize = RE_UV_FAC_BIT_LIST_SECRETS as usize / 4; + if i >= fachex.len() { + return Err(format!("Failure parsing UV facilities entry '{fachex}'.")); + } + let nibble = u32::from_str_radix(&fachex[i..i + 1], 16).unwrap(); + const THEBIT: u32 = 1 << (3 - (RE_UV_FAC_BIT_LIST_SECRETS % 4)); + if nibble & THEBIT == 0 { + return Err("The 'list secret' feature is missing on this UV.".to_string()); + } + Ok(()) + } +} + +/// Fetch the list of secrets from the UV. +/// Returns Err(errorstring) on error or +/// Ok(SecretList) on success. +/// The list may be empty if the UV doesn't have any secrets stored. +pub fn gather_secrets() -> Result { + let uv = match UvDevice::open() { + Err(e) => return Err(format!("Failed to open UV device: {:?}.", e)), + Ok(u) => u, + }; + let mut cmd = ListCmd::default(); + match uv.send_cmd(&mut cmd).map_err(|e| format!("{e:?}"))? { + UvcSuccess::RC_SUCCESS => (), + UvcSuccess::RC_MORE_DATA => println!("Warning: There is more data available than expected"), + }; + cmd.try_into().map_err(|e| format!("{e:?}")) +} + +#[cfg(test)] +mod tests { + + use super::*; + + // As the name says: check for list secrets feature bit in UV facilities. + #[test] + fn test_has_list_secrets_facility() { + let r = has_list_secrets_facility(); + if pv::misc::pv_guest_bit_set() { + assert!(r.is_ok()); + } else { + assert!(r.is_err()); + } + } + + // Simple invocation of the list_secrets function. Should not fail + #[test] + fn test_list_secrets() { + let r = gather_secrets(); + if pv::misc::pv_guest_bit_set() { + assert!(r.is_ok()); + } else { + assert!(r.is_err()); + } + } +}