mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
Get rid of 'read_hkd' by refactoring the 'get_verified_hkds' function. For this a new HkdLoader::load_and_verify is introduced that is a reworked version of the original code. In addition, add test cases for testing all the edge cases. Assisted-by: IBM Bob:1.0.6 Signed-off-by: Marc Hartmayer <marc@linux.ibm.com> Reviewed-by: Steffen Eiden <seiden@linux.ibm.com> Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
445 lines
13 KiB
Rust
445 lines
13 KiB
Rust
// 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<T> {
|
|
Auto,
|
|
Explicit(T),
|
|
}
|
|
|
|
impl<T> Display for AutoOrExplicit<T>
|
|
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<T> AutoOrExplicit<T> {
|
|
pub fn map<U, F>(self, f: F) -> AutoOrExplicit<U>
|
|
where
|
|
F: FnOnce(T) -> U,
|
|
{
|
|
match self {
|
|
AutoOrExplicit::Explicit(v) => AutoOrExplicit::Explicit(f(v)),
|
|
AutoOrExplicit::Auto => AutoOrExplicit::Auto,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T> FromStr for AutoOrExplicit<T>
|
|
where
|
|
T: FromStr<Err = String>,
|
|
{
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
|
match s {
|
|
"auto" => Ok(Self::Auto),
|
|
_ => {
|
|
let v = T::from_str(s)?;
|
|
Ok(Self::Explicit(v))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct AutoOrExplicitParser<T> {
|
|
_marker: PhantomData<T>,
|
|
}
|
|
|
|
impl<T> Default for AutoOrExplicitParser<T> {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl<T> AutoOrExplicitParser<T> {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
_marker: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T> TypedValueParser for AutoOrExplicitParser<T>
|
|
where
|
|
T: ValueEnum + FromStr + Clone + Send + Sync + Display + 'static,
|
|
T::Err: std::fmt::Display,
|
|
{
|
|
type Value = AutoOrExplicit<T>;
|
|
|
|
fn parse_ref(
|
|
&self,
|
|
cmd: &Command,
|
|
arg: Option<&Arg>,
|
|
value: &std::ffi::OsStr,
|
|
) -> Result<Self::Value, clap::error::Error> {
|
|
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::<T>().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<Box<dyn Iterator<Item = PossibleValue> + '_>> {
|
|
let enum_parser = EnumValueParser::<T>::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<PathBuf>,
|
|
|
|
/// 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<PathBuf>,
|
|
|
|
/// 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<PathBuf>,
|
|
|
|
/// 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<PathBuf>,
|
|
}
|
|
|
|
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<Box<dyn HkdVerifier>> {
|
|
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<Vec<HostKey>> {
|
|
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<P: AsRef<Path>>(path: P) -> Result<Box<dyn Write>> {
|
|
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<P: AsRef<Path>>(path: P) -> Result<Box<dyn Read>> {
|
|
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: &E, verbosity: LevelFilter) -> ExitCode
|
|
where
|
|
// Error trait is not required, but here to limit the usage to errors
|
|
E: AsRef<dyn std::error::Error> + 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<str>, P: AsRef<str>>(
|
|
named: &'a Option<N>,
|
|
positional: &'a Option<P>,
|
|
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<str>, P: AsRef<str>>(
|
|
named: &'a Option<N>,
|
|
positional: &'a Option<P>,
|
|
) -> &'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());
|
|
}
|
|
}
|
|
}
|