// SPDX-License-Identifier: MIT // // Copyright IBM Corp. use std::fmt::Display; use std::io::{Read, Write}; use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::process::ExitCode; use std::str::FromStr; use clap::builder::{EnumValueParser, PossibleValue, TypedValueParser}; use clap::{Arg, ArgAction, ArgGroup, Args, Command, ValueEnum, ValueHint}; use log::LevelFilter; use pv::misc::{create_file, open_file}; use pv::request::{HkdVerifier, HostKey}; use pv::Result; use crate::hkd::{HkdLoader, HkdVersionSelection}; /// Generic version selection for CLI #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AutoOrExplicit { Auto, Explicit(T), } impl Display for AutoOrExplicit where T: Display, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { AutoOrExplicit::Auto => write!(f, "auto"), AutoOrExplicit::Explicit(version) => write!(f, "{version}"), } } } impl AutoOrExplicit { pub fn map(self, f: F) -> AutoOrExplicit where F: FnOnce(T) -> U, { match self { AutoOrExplicit::Explicit(v) => AutoOrExplicit::Explicit(f(v)), AutoOrExplicit::Auto => AutoOrExplicit::Auto, } } } impl FromStr for AutoOrExplicit where T: FromStr, { type Err = String; fn from_str(s: &str) -> std::result::Result { match s { "auto" => Ok(Self::Auto), _ => { let v = T::from_str(s)?; Ok(Self::Explicit(v)) } } } } #[derive(Clone)] pub struct AutoOrExplicitParser { _marker: PhantomData, } impl Default for AutoOrExplicitParser { fn default() -> Self { Self::new() } } impl AutoOrExplicitParser { pub fn new() -> Self { Self { _marker: PhantomData, } } } impl TypedValueParser for AutoOrExplicitParser where T: ValueEnum + FromStr + Clone + Send + Sync + Display + 'static, T::Err: std::fmt::Display, { type Value = AutoOrExplicit; fn parse_ref( &self, cmd: &Command, arg: Option<&Arg>, value: &std::ffi::OsStr, ) -> Result { let s = value .to_str() .ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd))?; if s == format!("{}", Self::Value::Auto) { Ok(Self::Value::Auto) } else { let parsed = s.parse::().map_err(|_e| { let mut err = clap::error::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd); if let Some(arg) = arg { err.insert( clap::error::ContextKind::InvalidArg, clap::error::ContextValue::String(arg.to_string()), ); } err.insert( clap::error::ContextKind::InvalidValue, clap::error::ContextValue::String(s.to_string()), ); err })?; Ok(Self::Value::Explicit(parsed)) } } fn possible_values(&self) -> Option + '_>> { let enum_parser = EnumValueParser::::new(); let mut values = vec![PossibleValue::new("auto")]; values.extend(enum_parser.possible_values()?); Some(Box::new(values.into_iter())) } } /// CLI Argument collection for handling host-keys, IBM signing keys, and certificates. #[derive(Args, Debug, Clone, PartialEq, Eq, Default)] #[command( group(ArgGroup::new("pv_verify").required(true).args(["no_verify", "certs"])), )] pub struct CertificateOptions { /// Use FILE as a host-key document. /// /// Can be specified multiple times and must be specified at least once. #[arg( short = 'k', long = "host-key-document", value_name = "FILE", required = true, value_hint = ValueHint::FilePath, use_value_delimiter = true, value_delimiter = ',', )] pub host_key_documents: Vec, /// Disable the host-key document verification. /// /// Does not require the host-key documents to be valid. /// Do not use for a production request unless you verified the host-key document beforehand. #[arg(long)] pub no_verify: bool, /// Use FILE as a certificate to verify the host-key or keys. /// /// The certificates are used to establish a chain of trust for the verification /// of the host-key documents. Specify this option twice to specify the IBM Z signing key and /// the intermediate CA certificate (signed by the root CA). #[arg( short= 'C', long = "cert", value_name = "FILE", alias("crt"), value_hint = ValueHint::FilePath, use_value_delimiter = true, value_delimiter = ',', )] pub certs: Vec, /// Use FILE as a certificate revocation list (CRL). /// /// The list is used to check whether a certificate of the chain of /// trust is revoked. Specify this option multiple times to use multiple CRLs. #[arg( long = "crl", requires("certs"), value_name = "FILE", value_hint = ValueHint::FilePath, use_value_delimiter = true, value_delimiter = ',', )] pub crls: Vec, /// Make no attempt to download CRLs. #[arg(long, requires("certs"))] pub offline: bool, /// Use FILE as the root-CA certificate for the verification. /// /// If omitted, the system wide-root CAs installed on the system are used. /// Use this only if you trust the specified certificate. #[arg(long, requires("certs"))] pub root_ca: Option, } impl CertificateOptions { /// Returns the verifier of this [`CertificateOptions`] based on the given CLI options. /// /// - `protectee`: what you want to create. e.g. add-secret request or SE-image /// /// # Errors /// /// This function will return an error if [`crate::request::HkdVerifier`] cannot be created. fn verifier(&self, protectee: &'static str) -> Result> { use pv::request::{CertVerifier, NoVerifyHkd}; match self.no_verify { true => { log::warn!( "Host-key document verification is disabled. The {protectee} may not be protected." ); Ok(Box::new(NoVerifyHkd)) } false => Ok(Box::new(CertVerifier::new( &self.certs, &self.crls, self.root_ca.as_ref(), self.offline, )?)), } } /// Read the hybrid host-keys specified and verifies them if required /// /// - `protectee`: what you want to create. e.g. add-secret request or SE-image /// - `version`: requested host-key document version /// /// # Error /// Returns an error if something went wrong during parsing the HKDs, the verification chain /// could not built, or when the verification /// failed. pub fn get_verified_hkds( &self, protectee: &'static str, requested_version: HkdVersionSelection, ) -> Result> { let hkds = &self.host_key_documents; let verifier = self.verifier(protectee)?; let mut res = Vec::with_capacity(hkds.len()); for hkd in hkds { let host_key = HkdLoader::load_and_verify(hkd, verifier.as_ref(), requested_version)?; res.push(host_key); } Ok(res) } } /// stdout pub const STDOUT: &str = "-"; /// stdin pub const STDIN: &str = "-"; /// Converts an argument value into a Writer. pub fn get_writer_from_cli_file_arg>(path: P) -> Result> { if path.as_ref() == Path::new(STDOUT) { Ok(Box::new(std::io::stdout())) } else { Ok(Box::new(create_file(path)?)) } } /// Converts an argument value into a Reader. pub fn get_reader_from_cli_file_arg>(path: P) -> Result> { if path.as_ref() == Path::new(STDIN) { Ok(Box::new(std::io::stdin())) } else { Ok(Box::new(open_file(path)?)) } } /// Print an error that occurred during CLI parsing pub fn print_cli_error(e: clap::Error, mut cmd: Command) -> ExitCode { let ret = if e.use_stderr() { ExitCode::FAILURE } else { ExitCode::SUCCESS }; // Ignore any errors during printing of the error let _ = e.format(&mut cmd).print(); ret } /// Print an error to stderr pub fn print_error(e: &E, verbosity: LevelFilter) -> ExitCode where // Error trait is not required, but here to limit the usage to errors E: AsRef + std::fmt::Debug + std::fmt::Display, { if verbosity > LevelFilter::Warn { // Debug formatter also prints the whole error stack // So only print it when on verbose eprintln!("error: {e:?}") } else { eprintln!("error: {e}") }; ExitCode::FAILURE } #[derive(Args, Debug, Clone, Default)] pub struct VerbosityOptions { #[arg( long, short = 'v', action = ArgAction::Count, global = true, display_order = 999, )] /// Provide more detailed output. verbose: u8, #[arg( long, short = 'q', action = ArgAction::Count, global = true, conflicts_with = "verbose", display_order = 999, )] /// Provide less output. quiet: u8, } const fn to_level_filter(v: u8) -> LevelFilter { match v { 0 => LevelFilter::Off, 1 => LevelFilter::Error, 2 => LevelFilter::Warn, 3 => LevelFilter::Info, 4 => LevelFilter::Debug, 5.. => LevelFilter::Trace, } } impl VerbosityOptions { fn verbosity(&self) -> u8 { (LevelFilter::Warn as i16 + self.verbose as i16 - self.quiet as i16) .clamp(u8::MIN.into(), u8::MAX.into()) as u8 } pub fn to_level_filter(&self) -> LevelFilter { to_level_filter(self.verbosity()) } } #[derive(Args, Debug, Clone, Default)] pub struct DeprecatedVerbosityOptions { #[clap(flatten)] verbosity: VerbosityOptions, #[arg( short = 'V', action = ArgAction::Count, global = true, hide = true, )] /// Provide more detailed output. deprecated_verbose: u8, } impl DeprecatedVerbosityOptions { pub fn to_level_filter(&self) -> LevelFilter { if self.deprecated_verbose > 0 { // Use eprintln as the logger is most likely not yet initialized. eprintln!("WARNING: Use of deprecated flag '-V'. Use '-v' or '--verbose' instead.") } to_level_filter( self.verbosity .verbosity() .saturating_add(self.deprecated_verbose), ) } } pub fn combined_path_opt<'a, N: AsRef, P: AsRef>( named: &'a Option, positional: &'a Option

, default: &'a str, ) -> &'a str { match (named, positional) { (None, Some(i)) => i.as_ref(), (Some(i), None) => i.as_ref(), (Some(_), Some(_)) => unreachable!(), (None, None) => default, } } pub fn combined_path_req<'a, N: AsRef, P: AsRef>( named: &'a Option, positional: &'a Option

, ) -> &'a str { match (named, positional) { (None, Some(i)) => i.as_ref(), (Some(i), None) => i.as_ref(), (Some(_), Some(_)) => unreachable!(), (None, None) => unreachable!(), } } #[cfg(test)] mod test { use clap::Parser; use super::*; #[test] #[rustfmt::skip] fn cli_args() { //Verify only that some arguments are optional, we do not want to test clap, only the //configuration let valid_args = [vec!["pgr", "-k", "hkd.crt", "--no-verify"], vec!["pgr", "-k", "hkd.crt", "--crt", "abc.crt"]]; // Test for the minimal amount of flags to yield an invalid combination let invalid_args = [ vec!["pgr", "-k", "hkd.crt"], vec!["pgr", "--no-verify", "--crt", "abc.crt"], vec!["pgr", "--no-verify", "--crt", "abc.crt", "--offline"], vec!["pgr", "--no-verify", "--crt", "abc.crt", "--crl", "abc.crl"], vec!["pgr", "--no-verify", "--crt", "abc.crt", "--root-ca", "root.crt"], vec!["pgr", "--offline"], vec!["pgr", "--crl", "abc.crl"], vec!["pgr", "--root-ca", "root.crt"], ]; #[derive(Parser, Debug)] struct TestParser { #[command(flatten)] pub verify_args: CertificateOptions, } for arg in valid_args { let res = TestParser::try_parse_from(&arg); assert!(res.is_ok()); } for arg in invalid_args { let res = TestParser::try_parse_from(&arg); assert!(res.is_err()); } } }