mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
+ Sort and group the imports + Normalize and format comments (100 characters width) Command used: $ cargo +nightly fmt -- Acked-by: Steffen Eiden <seiden@linux.ibm.com> Signed-off-by: Marc Hartmayer <marc@linux.ibm.com> Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
366 lines
10 KiB
Rust
366 lines
10 KiB
Rust
// SPDX-License-Identifier: MIT
|
|
//
|
|
// Copyright IBM Corp.
|
|
|
|
//! # pvebc - Protected Virtualization Early Boot Customization Tool
|
|
//!
|
|
//! This tool processes AddSecretRequest files (toc.asr) that define the root of EBC
|
|
//! (Early Boot Customization) resources. It validates references to associated
|
|
//! toc.pol policies and manages their addition to the system.
|
|
//!
|
|
//! ## Features
|
|
//!
|
|
//! - Validates AddSecretRequest files and their associated policies
|
|
//! - Verifies cryptographic hashes of policy files using SHA-256
|
|
//! - Supports dry-run mode for validation without system changes
|
|
//! - MAC tag validation for AddSecretRequest files
|
|
|
|
#![allow(missing_docs)]
|
|
|
|
mod cli;
|
|
mod ebc_utils;
|
|
|
|
use std::fs::{self, File};
|
|
use std::io::{BufRead, Read};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::ExitCode;
|
|
use std::str::from_utf8;
|
|
|
|
use anyhow::{bail, Context, Error, Result};
|
|
use clap::Parser;
|
|
use pv_core::misc::{decode_hex, encode_hex};
|
|
#[cfg(target_arch = "s390x")]
|
|
use pv_core::uv::{AddCmd, UvDevice};
|
|
use pv_core::PolicyReference;
|
|
// Don't use openssl here because this tool is intended to run in the initramfs
|
|
// phase of the boot and there we don't want to dynamically link against a C lib
|
|
use sha2::{self, Digest};
|
|
#[cfg(target_arch = "s390x")]
|
|
use utils::get_reader_from_cli_file_arg;
|
|
use zerocopy::TryFromBytes;
|
|
|
|
use crate::cli::Cli;
|
|
use crate::ebc_utils::{get_data, get_mac_tag, get_reader};
|
|
|
|
/// Offset in bytes where user data starts in an ASRCB v1 structure
|
|
const V1_USER_DATA_OFFS: usize = 536;
|
|
/// Size in bytes of the user data field in an ASRCB
|
|
const USER_DATA_SIZE: usize = 512;
|
|
|
|
/// Validates and resolves a policy name relative to a base directory
|
|
///
|
|
/// # Errors
|
|
/// Returns an error if path traversal is detected or the resolved path
|
|
/// is outside the base directory
|
|
fn validate_and_resolve_policy_path(base: &Path, name: &str) -> Result<PathBuf> {
|
|
// Validate path to prevent directory traversal
|
|
if name.contains("..") || name.starts_with('/') {
|
|
bail!("Invalid policy name: path traversal detected in '{}'", name);
|
|
}
|
|
|
|
let resolved = base.join(name);
|
|
|
|
// Ensure the resulting path is within the expected directory
|
|
if !resolved.starts_with(base) {
|
|
bail!(
|
|
"Policy path '{}' is outside the expected directory",
|
|
resolved.display()
|
|
);
|
|
}
|
|
|
|
Ok(resolved)
|
|
}
|
|
|
|
/// Extracts and validates the policy name from a PolicyReference
|
|
///
|
|
/// # Errors
|
|
/// Returns an error if the policy name contains invalid UTF-8 or is empty
|
|
fn extract_policy_name(policy_ref: &PolicyReference) -> Result<String> {
|
|
let name = from_utf8(&policy_ref.name)
|
|
.context("Policy name contains invalid UTF-8")?
|
|
.trim_matches('\0')
|
|
.to_string();
|
|
|
|
if name.is_empty() {
|
|
bail!("Policy name is empty");
|
|
}
|
|
|
|
Ok(name)
|
|
}
|
|
|
|
/// Computes the SHA-256 hash of data from a reader.
|
|
///
|
|
/// Reads data from the provided reader in 4096-byte chunks and computes
|
|
/// the SHA-256 hash of the entire content.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// * `r` - A reader providing the data to hash
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns a `Vec<u8>` containing the 32-byte SHA-256 hash, or an error
|
|
/// if reading fails.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if reading from the reader fails.
|
|
pub fn sha256_hash<R: Read>(mut r: R) -> Result<Vec<u8>, Error> {
|
|
let mut hasher = sha2::Sha256::new();
|
|
let mut buf: [u8; 4096] = [0; 4096];
|
|
|
|
loop {
|
|
let read = r.read(&mut buf)?;
|
|
if read == 0 {
|
|
break;
|
|
}
|
|
hasher.update(&buf[..read]);
|
|
}
|
|
|
|
Ok(hasher.finalize().to_vec())
|
|
}
|
|
|
|
/// Extract the user data from an ASRCB
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the ASRCB is too small to contain user data
|
|
fn get_user_data(asrcb: &[u8]) -> Result<Option<Vec<u8>>> {
|
|
if asrcb.len() < V1_USER_DATA_OFFS + USER_DATA_SIZE {
|
|
bail!(
|
|
"ASRCB too small (expected at least {} bytes, got {})",
|
|
V1_USER_DATA_OFFS + USER_DATA_SIZE,
|
|
asrcb.len()
|
|
);
|
|
}
|
|
|
|
let user_data = &asrcb[V1_USER_DATA_OFFS..V1_USER_DATA_OFFS + USER_DATA_SIZE];
|
|
Ok(Some(user_data.to_vec()))
|
|
}
|
|
|
|
/// Get user data from AddSecretRequest
|
|
fn verify_user_data(filepath: &Path) -> Result<Option<Vec<u8>>> {
|
|
let mut rd_in = get_reader(filepath)?;
|
|
let data_in = get_data(&mut rd_in)?;
|
|
|
|
get_user_data(&data_in).context("Could not verify the Add-secret request")
|
|
}
|
|
|
|
/// Adds the given AddSecretRequest (if dryrun == false) and parses the contained user data
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// On Success returns the PolicyReference parsed from the given AddSecretRequest
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// returns an error if
|
|
/// - unable to open UvDevice
|
|
/// - reader from path fails
|
|
/// - adding of ASR fails
|
|
/// - verify_user_data fails
|
|
/// - unable to convert user data to PolicyReference
|
|
fn asr_to_pol_ref(filepath: &Path, dryrun: bool) -> Result<Option<PolicyReference>> {
|
|
print!("Add-Secret-Request: \"{}\"", filepath.display());
|
|
if !dryrun {
|
|
#[cfg(target_arch = "s390x")]
|
|
{
|
|
let uv = UvDevice::open()?;
|
|
let mut rd_in = get_reader_from_cli_file_arg(filepath)?;
|
|
let mut cmd = AddCmd::new(&mut rd_in)
|
|
.context(format!("Processing input file {:?}", filepath.to_str()))?;
|
|
uv.send_cmd(&mut cmd)?;
|
|
println!();
|
|
}
|
|
#[cfg(not(target_arch = "s390x"))]
|
|
{
|
|
println!(" (skip adding: not running on s390x architecture)");
|
|
}
|
|
} else {
|
|
println!(" (skip adding: dry-run mode)");
|
|
}
|
|
|
|
Ok(match verify_user_data(filepath)? {
|
|
Some(ud) => {
|
|
let ret = PolicyReference::try_read_from_bytes(&ud).map_err(|e| {
|
|
anyhow::anyhow!("Failed to parse PolicyReference from user data: {:?}", e)
|
|
})?;
|
|
println!(" Reference: {}", ret);
|
|
Some(ret)
|
|
}
|
|
None => None,
|
|
})
|
|
}
|
|
|
|
/// Read the mac tag list of the toc policy, parse and verify every tag
|
|
fn execute_toc_pol(path: &Path, toc_pol_ref: PolicyReference) -> Result<Vec<PathBuf>> {
|
|
let name = extract_policy_name(&toc_pol_ref)?;
|
|
let toc_path = validate_and_resolve_policy_path(path, &name)?;
|
|
let mut ret: Vec<PathBuf> = Vec::new();
|
|
let mut rd_in = get_reader(&toc_path)?;
|
|
let data_in = get_data(&mut rd_in)?;
|
|
|
|
let macs = data_in.lines();
|
|
let base = get_base_dir(&toc_path);
|
|
|
|
println!("Mac tags in {:?}:", toc_path);
|
|
for mac in macs {
|
|
let mut mac_tag = Vec::new();
|
|
let mac_tag_ref = match mac {
|
|
Ok(s) => s,
|
|
_ => continue,
|
|
};
|
|
print!(" {}", mac_tag_ref);
|
|
|
|
let entries = fs::read_dir(base)?;
|
|
for entry in entries {
|
|
let path = entry?;
|
|
let filepath = path.path();
|
|
let ext = match filepath.extension() {
|
|
Some(e) => e,
|
|
None => continue,
|
|
};
|
|
if ext == "asr" {
|
|
mac_tag = get_mac_tag(filepath.as_path())?;
|
|
|
|
if mac_tag == decode_hex(&mac_tag_ref)? {
|
|
println!(" -> {}", filepath.display());
|
|
ret.push(filepath.clone());
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if mac_tag != decode_hex(&mac_tag_ref)? {
|
|
bail!(
|
|
"No ASR with mac tag \"{}\" found in {:?}",
|
|
mac_tag_ref,
|
|
base
|
|
);
|
|
}
|
|
}
|
|
|
|
println!();
|
|
|
|
Ok(ret)
|
|
}
|
|
|
|
/// Checks whether the given hash in polref matches the actual hash of the referenced file
|
|
fn verify_policy(basename: &Path, polref: PolicyReference) -> Result<()> {
|
|
let name = extract_policy_name(&polref)?;
|
|
let filepath = validate_and_resolve_policy_path(basename, &name)?;
|
|
println!("Verify \"{}\"", filepath.display());
|
|
|
|
println!(" Referenced: {}", encode_hex(polref.hash));
|
|
let f = File::open(filepath.as_path())?;
|
|
|
|
let check_hash = sha256_hash(f)?;
|
|
println!(" Calculated: {}", encode_hex(&check_hash));
|
|
|
|
if check_hash != polref.hash {
|
|
bail!(
|
|
"{:?} ({}) does not match expected hash ({})",
|
|
filepath,
|
|
encode_hex(&check_hash),
|
|
encode_hex(polref.hash)
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// For every AddSecretRequest find the corresponding policy
|
|
fn execute_asrs(asrs: Vec<PathBuf>, dryrun: bool) -> Result<()> {
|
|
// loop over ASRs
|
|
for asr in asrs {
|
|
// get the referenced policy
|
|
let pol_ref = match asr_to_pol_ref(asr.as_path(), dryrun)? {
|
|
Some(pr) => pr,
|
|
None => continue,
|
|
};
|
|
|
|
if pol_ref.hash.iter().all(|&b| b == 0) {
|
|
continue;
|
|
}
|
|
|
|
let base = get_base_dir(asr.as_path());
|
|
let name = extract_policy_name(&pol_ref)?;
|
|
let _pol_path = validate_and_resolve_policy_path(base, &name)?;
|
|
|
|
// verify the integrity of the referenced policy
|
|
verify_policy(base, pol_ref)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the parent directory of the given file
|
|
fn get_base_dir(filepath: &Path) -> &Path {
|
|
match filepath.parent() {
|
|
Some(p) => p,
|
|
None => Path::new(""),
|
|
}
|
|
}
|
|
|
|
/// Print a given error and return the failure exit code
|
|
fn error_to_exit_code(err: Error) -> ExitCode {
|
|
eprintln!("Error: {}", err);
|
|
|
|
ExitCode::FAILURE
|
|
}
|
|
|
|
/// main function
|
|
pub fn main() -> ExitCode {
|
|
let opt: Cli = Cli::parse();
|
|
|
|
let dryrun = opt.dry_run;
|
|
|
|
if dryrun {
|
|
println!("Dry-run mode detected, skipping secret addition");
|
|
println!();
|
|
}
|
|
|
|
// get toc asr filepath - wrapper script has already copied files to tmpfs
|
|
let toc_asr = &opt.toc;
|
|
let basename = match toc_asr.parent() {
|
|
Some(p) => p,
|
|
None => {
|
|
return error_to_exit_code(Error::msg("Unable to get directory from specified path"))
|
|
}
|
|
};
|
|
|
|
// get the PolicyReference from toc.asr to toc.pol
|
|
let toc_pol = match asr_to_pol_ref(toc_asr, dryrun) {
|
|
Ok(o) => match o {
|
|
Some(r) => r,
|
|
None => {
|
|
return error_to_exit_code(Error::msg(
|
|
"There is no linked policy in the supplied ASR",
|
|
))
|
|
}
|
|
},
|
|
Err(e) => return error_to_exit_code(e),
|
|
};
|
|
|
|
// verify the integrity on the referenced toc.pol
|
|
if let Err(e) = verify_policy(basename, toc_pol) {
|
|
return error_to_exit_code(e);
|
|
}
|
|
|
|
println!();
|
|
|
|
// get and verify ASRs from mac list of toc policy
|
|
let asr_list = match execute_toc_pol(basename, toc_pol) {
|
|
Ok(v) => v,
|
|
Err(e) => return error_to_exit_code(e),
|
|
};
|
|
|
|
// execute ASRs
|
|
if let Err(e) = execute_asrs(asr_list, dryrun) {
|
|
return error_to_exit_code(e);
|
|
}
|
|
|
|
ExitCode::SUCCESS
|
|
}
|