// 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) -> Self { Self(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 = Self::read_yaml_file(fname)?; Self::validate(&mut apconfig)?; Ok(Self(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.is_empty()); 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" ); } }