rust/pvapconfig: Introduce new tool pvapconfig

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 <freude@linux.ibm.com>
Reviewed-by: Ingo Franzki <ifranzki@linux.ibm.com>
Reviewed-by: Holger Dengler <dengler@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
This commit is contained in:
Harald Freudenberger
2023-12-01 12:10:20 +01:00
committed by Jan Höppner
parent 0764460eaf
commit 94a38ebc3a
12 changed files with 2536 additions and 1 deletions

16
rust/Cargo.lock generated
View File

@@ -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"

View File

@@ -1,6 +1,7 @@
[workspace]
members = [
"pv",
"pvapconfig",
"pvsecret",
"utils",
]

View File

@@ -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))

View File

@@ -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 <freude@linux.ibm.com>"]
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" }

View File

@@ -0,0 +1,9 @@
<!--
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.
-->
# pvapconfig
## Description
**pvapconfig** is used to automatically set up the AP configuration
within an IBM Secure Execution guest.

View File

@@ -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 <configfile>
Use <configfile> 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)

817
rust/pvapconfig/src/ap.rs Normal file
View File

@@ -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<ApqnInfo, String> {
Ok(ApqnInfo::Accel(ApqnInfoAccel {}))
}
fn cca_info(carddir: &str, queuedir: &str) -> Result<ApqnInfo, String> {
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<ApqnInfo, String> {
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<ApqnInfo, String> {
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<ApqnInfo>,
}
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<BindState, String> {
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<AssocState, String> {
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<Apqn>);
impl ApqnList {
#[cfg(test)] // only used in test code
pub fn from_apqn_vec(apqns: Vec<Apqn>) -> ApqnList {
ApqnList(apqns)
}
#[cfg(test)] // only used in test code
pub fn to_apqn_vec(&self) -> Vec<Apqn> {
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<ApqnList> {
let mut apqns: Vec<Apqn> = 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::<u32>().unwrap();
let mode = match caps.get(2).unwrap().as_str().parse::<char>().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<BindState, String> {
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<AssocState, String> {
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::<u16>();
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());
}
}
}

View File

@@ -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<String>,
/// 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
}
}

View File

@@ -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<ApConfigEntry>);
impl ApConfigList {
#[cfg(test)] // only used in test code
pub fn from_apconfigentry_vec(apconfigs: Vec<ApConfigEntry>) -> 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<Vec<ApConfigEntry>, 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<ApConfigList, String> {
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<String, String> {
let dir = env::temp_dir();
let rnd = rand::random::<u32>();
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"
);
}
}

View File

@@ -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<u8> {
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<String, Box<dyn Error>> {
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<dyn Error>> {
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<i32, Box<dyn Error>> {
let content = sysfs_read_string(fname)?;
Ok(content.parse::<i32>()?)
}
/// Write an i32 value into a sysfs file
pub fn sysfs_write_i32(fname: &str, value: i32) -> Result<(), Box<dyn Error>> {
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<Vec<String>, Box<dyn Error>> {
let mut v: Vec<String> = 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<Vec<String>, Box<dyn Error>> {
let mut v: Vec<String> = 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<Self, String> {
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());
}
}

668
rust/pvapconfig/src/main.rs Normal file
View File

@@ -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<usize, String> {
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::<u32>().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::<u32>().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<Apqn> {
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<SecretEntry> {
let mut v: Vec<SecretEntry> = 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<ApConfigEntry> {
let mut v: Vec<ApConfigEntry> = 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<Apqn> = Vec::new();
apqns.push(test_apqns[0].clone());
let secrets: Vec<SecretEntry> = Vec::new();
let secretlist = SecretList::new(secrets.len() as u16, secrets);
let test_apconfigs = make_test_apconfigs();
let mut apconfig: Vec<ApConfigEntry> = 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<Apqn> = 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<ApConfigEntry> = 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<Apqn> = 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<ApConfigEntry> = 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);
}
}

105
rust/pvapconfig/src/uv.rs Normal file
View File

@@ -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<SecretList, String> {
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());
}
}
}