mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
rust: Refactoring and reduce API surface
Prepare pv & pv_core crates to be released on crates.io: * Remove any unused API to stay flexible * Remove utils dependency * Move cli, tmpfile and version utilities to local utils crate * Use the new utilities in the pv tools * Rename Secret into Confidential to avoid confusion of Secret (now Confidential) and AddSecret requests. * Move the uvsecret module out of the request module and change the name to secret. * Cleanup dependencies * Precise and correct minimal dependency versions * Inline `Aes256Key::from_digest` The cleanup ensures that the code also compiles with the dependencies resolved to their minimal versions using: $ cargo +nightly -Z minimal-versions update $ cargo build For more information refer to this blog post: https://users.rust-lang.org/t/psa-please-specify-precise-dependency-versions-in-cargo-toml/71277/8 Signed-off-by: Marc Hartmayer <mhartmay@de.ibm.com> Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
This commit is contained in:
@@ -3,3 +3,9 @@ name = "utils"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap = { version ="4.1", features = ["derive", "wrap_help"] }
|
||||
libc = "0.2.49"
|
||||
log = { version = "0.4.6", features = ["std", "release_max_level_debug"] }
|
||||
pv = { path = "../pv" }
|
||||
|
||||
241
rust/utils/src/cli.rs
Normal file
241
rust/utils/src/cli.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp. 2023, 2024
|
||||
|
||||
use clap::{ArgGroup, Args, Command, ValueHint};
|
||||
use log::{info, warn};
|
||||
use pv::misc::read_file;
|
||||
use pv::{
|
||||
misc::{create_file, open_file, read_certs},
|
||||
request::{
|
||||
openssl::pkey::{PKey, Public},
|
||||
HkdVerifier,
|
||||
},
|
||||
Error, Result,
|
||||
};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use std::process::ExitCode;
|
||||
|
||||
/// CLI Argument collection for handling certificates.
|
||||
#[derive(Args, Debug, 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 used 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<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// Use FILE as a certificate revocation list.
|
||||
///
|
||||
/// 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<String>,
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
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.iter().map(Path::new).collect::<Vec<_>>(),
|
||||
&self.crls.iter().map(Path::new).collect::<Vec<_>>(),
|
||||
self.root_ca.as_ref().map(Path::new),
|
||||
self.offline,
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the host-keys specified and verifies them if required
|
||||
///
|
||||
/// - `protectee`: what you want to create. e.g. add-secret request or SE-image
|
||||
///
|
||||
/// # 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) -> Result<Vec<PKey<Public>>> {
|
||||
let hkds = &self.host_key_documents;
|
||||
let verifier = self.verifier(protectee)?;
|
||||
|
||||
let mut res = Vec::with_capacity(hkds.len());
|
||||
for hkd in hkds {
|
||||
let hk = read_file(hkd, "host-key document")?;
|
||||
let certs = read_certs(&hk).map_err(|source| Error::HkdNotPemOrDer {
|
||||
hkd: hkd.to_string(),
|
||||
source,
|
||||
})?;
|
||||
if certs.is_empty() {
|
||||
return Err(Error::NoHkdInFile(hkd.to_string()));
|
||||
}
|
||||
if certs.len() != 1 {
|
||||
warn!("The host-key document in '{hkd}' contains more than one certificate!")
|
||||
}
|
||||
|
||||
// Panic: len is == 1 -> unwrap will succeed/not panic
|
||||
let c = certs.first().unwrap();
|
||||
verifier.verify(c)?;
|
||||
res.push(c.public_key()?);
|
||||
info!("Use host-key document at '{hkd}'");
|
||||
}
|
||||
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: &str) -> Result<Box<dyn Write>> {
|
||||
if path == 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: &str) -> Result<Box<dyn Read>> {
|
||||
if path == STDIN {
|
||||
Ok(Box::new(std::io::stdin()))
|
||||
} else {
|
||||
Ok(Box::new(open_file(path)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Print an error that occured 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: u8) -> 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 > 0 {
|
||||
// Debug formatter also prints the whole error stack
|
||||
// So only print it when on verbose
|
||||
eprintln!("error: {e:?}")
|
||||
} else {
|
||||
eprintln!("error: {e}")
|
||||
};
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,17 @@
|
||||
//! Utils for s390-tools written in rust.
|
||||
//! Not intened to be used outside of s390-tools.
|
||||
//!
|
||||
//! Copyright IBM Corp. 2023
|
||||
//! Copyright IBM Corp. 2023, 2024
|
||||
mod cli;
|
||||
mod log;
|
||||
mod tmpfile;
|
||||
|
||||
pub use crate::cli::CertificateOptions;
|
||||
pub use crate::cli::{get_reader_from_cli_file_arg, get_writer_from_cli_file_arg};
|
||||
pub use crate::cli::{print_cli_error, print_error};
|
||||
pub use crate::cli::{STDIN, STDOUT};
|
||||
pub use crate::log::PvLogger;
|
||||
pub use crate::tmpfile::TemporaryDirectory;
|
||||
|
||||
/// Get the s390-tools release string
|
||||
///
|
||||
@@ -28,6 +38,33 @@ macro_rules! release_string {
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
/// Print the version to stdout
|
||||
///
|
||||
/// verbosity: integer if >0 more and more details printed
|
||||
/// feat: (optional) list of features
|
||||
/// rel_str: a string containig the release name
|
||||
macro_rules! print_version {
|
||||
($verbosity: expr, $year: expr $( ,$feat: expr)?) => {{
|
||||
println!(
|
||||
"{} version {}\nCopyright IBM Corp. {}",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
$crate::release_string!(),
|
||||
$year,
|
||||
);
|
||||
if $verbosity > 0 {
|
||||
$($feat.iter().for_each(|f| print!("{f} ")); println!("(compiled)");)?
|
||||
}
|
||||
if $verbosity > 1 {
|
||||
println!(
|
||||
"\n{}-crate {}",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
/// Asserts a constant expression evaluates to `true`.
|
||||
///
|
||||
/// If the expression is not evaluated to `true` the compilation will fail.
|
||||
|
||||
49
rust/utils/src/log.rs
Normal file
49
rust/utils/src/log.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp. 2023
|
||||
|
||||
use log::{self, Level, LevelFilter, Log, Metadata, Record};
|
||||
|
||||
/// A simple Logger that prints to stderr if the verbosity level is high enough.
|
||||
/// Prints log-level for Debug+Trace
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct PvLogger;
|
||||
|
||||
fn to_level(verbosity: u8) -> LevelFilter {
|
||||
match verbosity {
|
||||
// Error and Warn on by default
|
||||
0 => LevelFilter::Warn,
|
||||
1 => LevelFilter::Info,
|
||||
2 => LevelFilter::Debug,
|
||||
_ => LevelFilter::Trace,
|
||||
}
|
||||
}
|
||||
|
||||
impl PvLogger {
|
||||
/// Set self as the logger for this application.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error is returned if a logger has already been set.
|
||||
pub fn start(&'static self, verbosity: u8) -> Result<(), log::SetLoggerError> {
|
||||
log::set_logger(self).map(|()| log::set_max_level(to_level(verbosity)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Log for PvLogger {
|
||||
fn enabled(&self, _metadata: &Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
if record.level() > Level::Info {
|
||||
eprintln!("{}: {}", record.level(), record.args());
|
||||
} else {
|
||||
eprintln!("{}", record.args());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
150
rust/utils/src/tmpfile.rs
Normal file
150
rust/utils/src/tmpfile.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use std::{
|
||||
ffi::{CString, OsStr},
|
||||
os::unix::prelude::OsStrExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Rust wrapper for `libc::mkdtemp`
|
||||
fn mkdtemp<P: AsRef<Path>>(template: P) -> Result<PathBuf, std::io::Error> {
|
||||
let template_cstr = CString::new(template.as_ref().as_os_str().as_bytes())?;
|
||||
let template_raw = template_cstr.into_raw();
|
||||
unsafe {
|
||||
// SAFETY: template_raw is a valid CString because it was generated by
|
||||
// the `CString::new`.
|
||||
let ret = libc::mkdtemp(template_raw);
|
||||
|
||||
if ret.is_null() {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
// SAFETY: `template_raw` is still a valid CString because it was
|
||||
// generated by `CString::new` and modified by `libc::mkdtemp`.
|
||||
let path_cstr = std::ffi::CString::from_raw(template_raw);
|
||||
let path = OsStr::from_bytes(path_cstr.as_bytes());
|
||||
let path = std::path::PathBuf::from(path);
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This type creates a temporary directory that is automatically removed when
|
||||
/// it goes out of scope. It utilizes the `mkdtemp` function and its semantics,
|
||||
/// with the addition of automatically including the template characters
|
||||
/// `XXXXXX`.
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct TemporaryDirectory {
|
||||
path: Box<Path>,
|
||||
}
|
||||
|
||||
impl TemporaryDirectory {
|
||||
/// Creates a temporary directory using `prefix` as directory prefix.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error is returned if the temporary directory could not be created.
|
||||
pub fn new<P: AsRef<Path>>(prefix: P) -> Result<Self, std::io::Error> {
|
||||
let mut template = prefix.as_ref().to_owned();
|
||||
let template_os_string = template.as_mut_os_string();
|
||||
template_os_string.push("XXXXXX");
|
||||
|
||||
let temp_dir = mkdtemp(template_os_string)?;
|
||||
Ok(Self {
|
||||
path: temp_dir.into_boxed_path(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the path of the created temporary directory.
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_ref()
|
||||
}
|
||||
|
||||
fn forget(mut self) {
|
||||
self.path = PathBuf::new().into_boxed_path();
|
||||
std::mem::forget(self);
|
||||
}
|
||||
|
||||
/// Removes the created temporary directory and it's contents.
|
||||
pub fn close(self) -> std::io::Result<()> {
|
||||
let ret = std::fs::remove_dir_all(&self.path);
|
||||
self.forget();
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for TemporaryDirectory {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.path()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TemporaryDirectory {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{mkdtemp, TemporaryDirectory};
|
||||
|
||||
#[test]
|
||||
fn mkdtemp_test() {
|
||||
let template_inv_not_last_characters = "XXXXXXyay";
|
||||
let template_inv_too_less_x = "yayXXXXX";
|
||||
let template_inv_path_does_not_exist = "../NA-yay/XXXXXX";
|
||||
|
||||
let template = "yayXXXXXX";
|
||||
|
||||
let _err = mkdtemp(template_inv_not_last_characters).expect_err("invalid template");
|
||||
let _err = mkdtemp(template_inv_too_less_x).expect_err("invalid template");
|
||||
let _err =
|
||||
mkdtemp(template_inv_path_does_not_exist).expect_err("path does not exist template");
|
||||
|
||||
let path = mkdtemp(template).expect("mkdtemp should work");
|
||||
assert!(path.exists());
|
||||
assert!(path.as_os_str().to_str().expect("works").starts_with("yay"));
|
||||
std::fs::remove_dir(path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporary_directory_empty_name_test() {
|
||||
let temp_dir = TemporaryDirectory::new("").expect("should work");
|
||||
let path = temp_dir.path().to_owned();
|
||||
assert!(path.exists());
|
||||
|
||||
// Test that close removes the directory
|
||||
temp_dir.close().unwrap();
|
||||
assert!(!path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporary_directory_drop_test() {
|
||||
let temp_dir = TemporaryDirectory::new("").expect("should work");
|
||||
let path = temp_dir.path().to_owned();
|
||||
assert!(path.exists());
|
||||
|
||||
// Test that the destructor removes the directory
|
||||
drop(temp_dir);
|
||||
assert!(!path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporary_directory_close_test() {
|
||||
let temp_dir = TemporaryDirectory::new("yay").expect("should work");
|
||||
|
||||
let path = temp_dir.path().to_owned();
|
||||
assert!(path.exists());
|
||||
assert!(path.as_os_str().to_str().expect("works").starts_with("yay"));
|
||||
|
||||
// Test that close() removes the directory
|
||||
temp_dir.close().unwrap();
|
||||
assert!(!path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporary_directory_as_ref_test() {
|
||||
let temp_dir = TemporaryDirectory::new("").expect("should work");
|
||||
|
||||
assert_eq!(temp_dir.path(), temp_dir.as_ref());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user