mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
rust: Add new tool pvebc
Add pvebc tool for parsing and verifying EBC Add-Secret-Request structures Introduce pvebc, a CLI tool that parses and verifies the integrity of Add-Secret-Request (ASR) structures used in Early Boot Customization for SEL guests. The tool processes an integrity-protected ASR structure consisting of: - toc.asr: Meta secret that links to toc.pol via relative filepath and SHA512 hash, integrity-protected by its AES GCM authentication tag - toc.pol: Policy file containing AES GCM authentication tags (last 16 bytes) of all user-provided ASRs - User ASRs: Individual Add-Secret-Requests containing encrypted secrets This structure guarantees: - Prevents ASR removal: toc.pol lists all expected ASR authentication tags - Prevents ASR insertion: Unlisted ASRs are rejected - Prevents ASR modification: AES GCM authentication tags provide cryptographic integrity - Prevents toc.pol tampering: toc.asr's integrity protection secures the link The tool verifies completeness by checking that all ASRs listed in toc.pol are present and their AES GCM authentication tags match. This prevents attackers from removing, inserting, or modifying ASRs during transport over unsecured channels. After verification, pvebc adds all ASRs to the Ultravisor (UV), which decrypts them using the guest's secret key and makes them available to the guest during early boot. Assisted-by: IBM Bob:1.0.1 Acked-by: Holger Dengler <dengler@linux.ibm.com> Reviewed-by: Steffen Eiden <seiden@linux.ibm.com> Signed-off-by: Finn Callies <fcallies@linux.ibm.com> Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
This commit is contained in:
committed by
Jan Höppner
parent
c803cb925e
commit
c04a0919f6
@@ -30,6 +30,8 @@ Package contents
|
||||
Manage secrets for IBM Secure Execution guests
|
||||
- pvimg:
|
||||
Create and inspect IBM Secure Execution images
|
||||
- pvebc:
|
||||
Verify a secret structure for IBM Secure Execution for Linux.
|
||||
|
||||
* dasdfmt:
|
||||
Low-level format ECKD DASDs with the classical Linux disk layout or the new
|
||||
|
||||
105
rust/Cargo.lock
generated
105
rust/Cargo.lock
generated
@@ -62,9 +62,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.95"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@@ -111,6 +111,15 @@ dependencies = [
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@@ -212,6 +221,25 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curl"
|
||||
version = "0.4.49"
|
||||
@@ -302,6 +330,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum_dispatch"
|
||||
version = "0.3.13"
|
||||
@@ -363,6 +401,16 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
@@ -404,6 +452,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@@ -641,6 +695,20 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pvebc"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"hex",
|
||||
"s390_pv_core",
|
||||
"sha2",
|
||||
"utils",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pvimg"
|
||||
version = "0.12.0"
|
||||
@@ -687,6 +755,7 @@ dependencies = [
|
||||
"s390_pv",
|
||||
"serde_yaml",
|
||||
"utils",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -891,6 +960,7 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_test",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"zerocopy",
|
||||
]
|
||||
@@ -969,6 +1039,17 @@ dependencies = [
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -1069,6 +1150,12 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "unarray"
|
||||
version = "0.1.4"
|
||||
@@ -1112,6 +1199,12 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.0"
|
||||
@@ -1238,18 +1331,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.25"
|
||||
version = "0.8.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.25"
|
||||
version = "0.8.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -5,6 +5,7 @@ members = [
|
||||
"pv_core",
|
||||
"pvapconfig",
|
||||
"pvattest",
|
||||
"pvebc",
|
||||
"pvimg",
|
||||
"pvinfo",
|
||||
"pvsecret",
|
||||
|
||||
@@ -11,7 +11,7 @@ PV_TARGETS :=
|
||||
CARGO_TEST_TARGETS :=
|
||||
|
||||
ifneq (${HAVE_CARGO},0)
|
||||
CARGO_TARGETS :=
|
||||
CARGO_TARGETS := pvebc
|
||||
|
||||
BUILD_TARGETS = $(CARGO_TARGETS)
|
||||
INSTALL_TARGETS := install-rust-tools install-man install-shell-completions
|
||||
|
||||
22
rust/pvebc/Cargo.toml
Normal file
22
rust/pvebc/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "pvebc"
|
||||
version = "0.12.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
pv_core = { path = "../pv_core" , package = "s390_pv_core" }
|
||||
utils = { path = "../utils" }
|
||||
anyhow = { version = "1.0.95", features = ["std"] }
|
||||
zerocopy = { version = "0.8.27", features = ["derive"] }
|
||||
|
||||
[build-dependencies]
|
||||
clap = { version ="4.5", features = ["derive"]}
|
||||
clap_complete = "4.5"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
165
rust/pvebc/man/pvebc.1
Normal file
165
rust/pvebc/man/pvebc.1
Normal file
@@ -0,0 +1,165 @@
|
||||
.\" Copyright IBM Corp.
|
||||
.\" s390-tools is free software; you can redistribute it and/or modify
|
||||
.\" it under the terms of the MIT license. See LICENSE for details.
|
||||
.\"
|
||||
|
||||
.TH "PVEBC" "1" "2026-03-24" "s390-tools" "EBC Management Manual"
|
||||
.nh
|
||||
.ad l
|
||||
.SH NAME
|
||||
pvebc \- Protected Virtualization Early Boot Customization Tool
|
||||
.SH SYNOPSIS
|
||||
.nf
|
||||
.fam C
|
||||
pvebc [OPTIONS] \-\-toc <FILE>
|
||||
.fam C
|
||||
.fi
|
||||
.SH DESCRIPTION
|
||||
Use the \fBpvebc\fR tool to process add-secret request files (\fBtoc.asr\fR)
|
||||
that define the root of early boot customization (EBC) resources for
|
||||
IBM Secure Execution for Linux (SEL) guests. The tool validates the
|
||||
integrity and completeness of add-secret request (ASR) files using
|
||||
cryptographic verification to prevent tampering during transmission
|
||||
over unsecured channels.
|
||||
|
||||
The tool operates on a structure consisting of three types of files:
|
||||
.RS 4
|
||||
\- \fBtoc.asr\fP: The root add\-secret request file that references the table of contents (TOC policy).
|
||||
|
||||
\- \fBtoc.pol\fP: The table of contents policy file that contains a list of AES\-GCM authentication tags (MAC tags)
|
||||
|
||||
\- \fBASR files\fP: Individual add-secret request files to be added to the guest
|
||||
|
||||
.RE
|
||||
The integrity protection mechanism works as follows:
|
||||
.RS 4
|
||||
.IP "1." 3
|
||||
Each add-secret request file includes an AES\-GCM authentication tag (last 16 bytes), which provides integrity
|
||||
protection for the file.
|
||||
.IP "2." 3
|
||||
The toc.pol file lists the expected message authentication code (MAC) tags of all add-secret request files.
|
||||
.IP "3." 3
|
||||
The toc.asr file contains a cryptographically protected reference to toc.pol, consisting
|
||||
of the relative file path and a SHA\-512 hash of the policy file.
|
||||
.IP "4." 3
|
||||
The AES\-GCM authentication tag of toc.asr protects the integrity of this reference.
|
||||
.RE
|
||||
|
||||
This structure prevents attackers from:
|
||||
.RS 4
|
||||
\- Removing add-secret request files from the set
|
||||
|
||||
\- Inserting unauthorized add-secret request files
|
||||
|
||||
\- Modifying existing add-secret request files
|
||||
|
||||
\- Altering the policy file
|
||||
|
||||
.RE
|
||||
\fBpvebc\fR validates the entire chain of trust by:
|
||||
.RS 4
|
||||
\- Verifying that the SHA\-512 hash of toc.pol matches the reference stored in toc.asr
|
||||
|
||||
\- Checking that all MAC tags listed in toc.pol correspond to existing add-secret request files
|
||||
|
||||
\- Ensuring that all add-secret request files are present and unmodified
|
||||
|
||||
.RE
|
||||
The tool can run in one of two modes:
|
||||
.RS 4
|
||||
\- \fBNormal mode\fP: Validates add\-secret requests and adds the defined secrets to the SEL guest (z/Architecture only)
|
||||
|
||||
\- \fBDry\-run mode\fP: Validates the request structure without adding secrets; useful for verification on non\-SEL systems
|
||||
|
||||
.RE
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\-t, \-\-toc <FILE>
|
||||
.RS 4
|
||||
Specify the table\-of\-contents add\-secret request file toc.asr, which serves as the root of the EBC
|
||||
resources. This file must contain a reference to toc.pol in its user data field,
|
||||
as generated by \fBpvsecret\fR with the \fB\-\-policy\fR option. The toc.asr
|
||||
file cryptographically links to the policy file by using a relative path and
|
||||
SHA\-512 hash, ensuring the integrity of the complete EBC structure.
|
||||
.RE
|
||||
.RE
|
||||
.PP
|
||||
\-\-dry\-run
|
||||
.RS 4
|
||||
Validate the EBC structure without adding the add\-secret requests to the
|
||||
ultravisor. Use this option to verify the integrity of the generated
|
||||
policy file and the links between add\-secret request files and the policy.
|
||||
This mode can be run on non\-SEL guests to validate the
|
||||
structure before deployment. When \fB\-\-dry\-run\fR is specified, the tool
|
||||
performs all cryptographic verifications but skips the actual addition of
|
||||
secrets to the ultravisor secret store.
|
||||
.RE
|
||||
.RE
|
||||
.PP
|
||||
\-\-version
|
||||
.RS 4
|
||||
Print version information and exit.
|
||||
.RE
|
||||
.RE
|
||||
.PP
|
||||
\-h, \-\-help
|
||||
.RS 4
|
||||
Print help information.
|
||||
.RE
|
||||
.RE
|
||||
|
||||
.SH FILES
|
||||
.PP
|
||||
The \fBpvebc\fR tool operates on the following file types (actual names may
|
||||
differ):
|
||||
.PP
|
||||
\fBtoc.asr\fR
|
||||
.RS 4
|
||||
The root add\-secret request file that contains a reference to the table\-of\-content \fBtoc.pol\fR in its
|
||||
user data field. This file is generated by using \fBpvsecret create\fR with the
|
||||
\fB\-\-policy\fR option.
|
||||
.RE
|
||||
.PP
|
||||
\fBtoc.pol\fR
|
||||
.RS 4
|
||||
The policy file containing a newline\-separated list of AES\-GCM authentication
|
||||
tags (MAC tags) in hexadecimal format. Each entry corresponds to one
|
||||
add\-secret request file.
|
||||
This file is generated by using \fBpvsecret create\fR with the
|
||||
\fB\-\-toc\-policy\fR option.
|
||||
.RE
|
||||
.PP
|
||||
\fB*.asr\fR
|
||||
.RS 4
|
||||
Individual add\-secret request files that contain the actual secrets to be added to
|
||||
the SEL guest. Each add\-secret request file includes an AES\-GCM authentication tag (the last 16 bytes), which
|
||||
provides integrity protection.
|
||||
.RE
|
||||
|
||||
.SH EXIT STATUS
|
||||
.PP
|
||||
\fBpvebc\fR returns the following exit codes:
|
||||
.PP
|
||||
\fB0\fR
|
||||
.RS 4
|
||||
Success. All validations passed and secrets were added (if not in dry\-run mode).
|
||||
.RE
|
||||
.PP
|
||||
\fB1\fR
|
||||
.RS 4
|
||||
Failure. An error occurred during validation or secret addition. Error details
|
||||
are printed to stderr.
|
||||
.RE
|
||||
|
||||
.SH NOTES
|
||||
.PP
|
||||
\- All file paths in a policy reference must be relative to the directory
|
||||
that contains the referencing file.
|
||||
.PP
|
||||
\- On z/Architecture systems, the tool requires access to the ultravisor device
|
||||
(\fB/dev/uv\fR) to add secrets.
|
||||
.PP
|
||||
|
||||
.SH "SEE ALSO"
|
||||
.sp
|
||||
\fBpvsecret\fR(1), \fBpvsecret\-create\fR(1), \fBpvsecret\-add\fR(1)
|
||||
39
rust/pvebc/src/cli.rs
Normal file
39
rust/pvebc/src/cli.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp.
|
||||
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
|
||||
use clap::{ArgAction, Parser, ValueHint};
|
||||
|
||||
static VERSION: OnceLock<String> = OnceLock::new();
|
||||
|
||||
/// The pvebc command processes an AddSecretRequest file (toc.asr) that defines the root of EBC
|
||||
/// resources. It validates references to the associated toc.pol policy and manages their addition,
|
||||
/// with options for verification and dry-run execution.
|
||||
#[derive(Parser)]
|
||||
#[command(long_version=ver(), disable_version_flag(true))]
|
||||
pub struct Cli {
|
||||
/// Print version information and exit.
|
||||
#[arg(long, action=ArgAction::Version)]
|
||||
version: (),
|
||||
|
||||
/// Specifies the toc.asr which is the root of the EBC resources
|
||||
///
|
||||
/// Specify the add-secret request file toc.asr that serves as the root of the EBC resources.
|
||||
/// Its user data must contain a reference to toc.pol as generated by pvsecret with the --policy
|
||||
/// option.
|
||||
#[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath)]
|
||||
pub toc: PathBuf,
|
||||
|
||||
/// Prevents adding of the AddSecretRequest
|
||||
///
|
||||
/// Do not add the AddSecretRequest. Use this option to validate a generated policy. It can be
|
||||
/// run on a non-SEL guest to verify ASR-to-policy links and the associated policy toc.pol.
|
||||
#[arg(long)]
|
||||
pub dry_run: bool,
|
||||
}
|
||||
|
||||
fn ver() -> &'static str {
|
||||
VERSION.get_or_init(|| utils::tools_version_fmt!(2026))
|
||||
}
|
||||
54
rust/pvebc/src/ebc_utils.rs
Normal file
54
rust/pvebc/src/ebc_utils.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp.
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use pv_core::misc::open_file;
|
||||
use std::{io::Read, path::Path};
|
||||
|
||||
/// Length of the MAC tag in bytes (last 16 bytes of AddSecretRequest files)
|
||||
pub const MAC_TAG_LEN: usize = 16;
|
||||
|
||||
/// Opens a file and returns a boxed reader
|
||||
pub fn get_reader_from_filepath<P: AsRef<Path>>(filepath: P) -> Result<Box<dyn Read>> {
|
||||
Ok(Box::new(open_file(filepath)?))
|
||||
}
|
||||
|
||||
/// Get reader from &Path with additional context on error
|
||||
pub fn get_reader(filepath: &Path) -> Result<Box<dyn Read>> {
|
||||
get_reader_from_filepath(filepath)
|
||||
.with_context(|| format!("unable to get reader from {:?}", filepath))
|
||||
}
|
||||
|
||||
/// Read all data from a reader into a Vec<u8>
|
||||
pub fn get_data(rd_in: &mut Box<dyn Read>) -> Result<Vec<u8>> {
|
||||
let mut data_in = Vec::new();
|
||||
rd_in
|
||||
.read_to_end(&mut data_in)
|
||||
.context("Cannot read input file")?;
|
||||
|
||||
Ok(data_in)
|
||||
}
|
||||
|
||||
/// Extract the MAC tag (last 16 bytes) from an AddSecretRequest file
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The file cannot be read
|
||||
/// - The file is smaller than MAC_TAG_LEN bytes
|
||||
pub fn get_mac_tag(filepath: &Path) -> Result<Vec<u8>> {
|
||||
let mut rd_in = get_reader(filepath)?;
|
||||
let data_in = get_data(&mut rd_in)?;
|
||||
|
||||
if data_in.len() < MAC_TAG_LEN {
|
||||
bail!(
|
||||
"File {:?} too small to contain MAC tag (expected at least {} bytes, got {})",
|
||||
filepath,
|
||||
MAC_TAG_LEN,
|
||||
data_in.len()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(data_in[data_in.len() - MAC_TAG_LEN..].to_vec())
|
||||
}
|
||||
369
rust/pvebc/src/main.rs
Normal file
369
rust/pvebc/src/main.rs
Normal file
@@ -0,0 +1,369 @@
|
||||
// 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;
|
||||
|
||||
#[cfg(target_arch = "s390x")]
|
||||
use pv_core::uv::{AddCmd, UvDevice};
|
||||
#[cfg(target_arch = "s390x")]
|
||||
use utils::get_reader_from_cli_file_arg;
|
||||
|
||||
use anyhow::{bail, Context, Error, Result};
|
||||
use clap::Parser;
|
||||
use pv_core::{
|
||||
misc::{decode_hex, encode_hex},
|
||||
PolicyReference,
|
||||
};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{BufRead, Read},
|
||||
path::{Path, PathBuf},
|
||||
process::ExitCode,
|
||||
str::from_utf8,
|
||||
};
|
||||
// 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};
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user