Files
s390-tools/rust/pvsecret/src/cli.rs
Finn Callies c803cb925e rust: Add toc support for ebc to pvsecret
Add two new command-line options to pvsecret create for Early Boot
Customization (EBC) Table of Contents (TOC) support:

 1. --policy FILE
    Links an Add-Secret-Request (ASR) to a policy file by embedding a
    PolicyReference in the ASR's user data field. The PolicyReference
    contains the relative file path and SHA512 hash of the policy file,
    enabling integrity verification of the policy. This option conflicts
    with --user-data as both use the same user data field in the ASR
    structure.

 2. --toc-policy FILE
    Appends the AES-GCM authentication tag (MAC tag - last 16 bytes of
    the encrypted ASR) to the specified TOC policy file. This enables
    the TOC policy to maintain a list of all ASR MAC tags for
    completeness verification during boot. The TOC can verify that all
    expected ASRs are present and unmodified by checking their MAC tags
    against this list. This option also conflicts with --user-data.

Both options support the EBC multi-party workflow where an ISV/CSP builds
a generic SEL image and customers customize it with their own secrets. The
TOC mechanism ensures the integrity and completeness of all EBC resources
during the boot process.

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>
2026-04-30 13:46:31 +02:00

545 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2023, 2024
use std::fmt::Display;
use clap::error::ErrorKind::ValueValidation;
use clap::{ArgGroup, Args, CommandFactory, Parser, Subcommand, ValueEnum, ValueHint};
use utils::{CertificateOptions, DeprecatedVerbosityOptions, STDOUT};
/// Manage secrets for IBM Secure Execution guests.
///
/// Use to create and send add-secret requests, list the added secrets and lock the Secret Store.
#[derive(Parser, Debug)]
pub struct CliOptions {
#[clap(flatten)]
pub verbosity: DeprecatedVerbosityOptions,
/// Print version information and exit.
// Implemented for the help message only. Actual parsing happens in the
// version command.
#[arg(long)]
pub version: bool,
#[command(subcommand)]
pub cmd: Command,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum CreateSecretFlags {
/// Disables host-initiated dumping for the target guest instance.
DisableDump,
}
#[derive(Args, Debug)]
#[command(group(ArgGroup::new("as-ext").args(["cck", "extension_secret"])),)]
pub struct CreateSecretOpt {
#[command(flatten)]
pub certificate_args: CertificateOptions,
/// Specifies the header of the guest image.
///
/// Can be an IBM Secure Execution image created by 'pvimg/genprotimg' or an
/// extracted IBM Secure Execution header.
#[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath)]
pub hdr: String,
/// Force the generation of add-secret requests on IBM Secure Execution guests.
///
/// If the program detects that it is running on an IBM Secure Execution guest, it denies the
/// generation of add-secret requests. The force flag overwrites this behavior.
#[arg(short, long)]
pub force: bool,
/// Write the generated request to FILE.
#[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath,)]
pub output: String,
/// Use the content of FILE as an extension secret.
///
/// The file must be exactly 32 bytes long. If this request is the first, all subsequent
/// requests must have the same extension secret. Only makes sense if bit 1 of the secret
/// control flags of the IBM Secure Execution header is
/// 0. Otherwise the ultravisor rejects the request.
#[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath,)]
pub extension_secret: Option<String>,
/// Use the content of FILE as the customer-communication key (CCK) to derive the extension
/// secret.
///
/// The file must contain exactly 32 bytes of data. If the target guest was started
/// with bit 1 of the secret control flag set, the ultravisor also derives the secret from the
/// CCK. Otherwise, the ultravisor interprets the extension secret as a normal one. This still
/// works if you use the same CCK for all requests.
#[arg(long, value_name = "FILE")]
pub cck: Option<String>,
/// Use HEXSTRING as the Configuration Unique ID.
///
/// Must be a hex 128-bit unsigned big endian number string. Leading zeros must be provided. If
/// specified, the value must match with the Config-UID from the attestation result of that
/// guest. If not specified, the CUID will be ignored by the ultravisor during the
/// verification of the request.
#[arg(long, value_name = "HEXSTRING")]
pub cuid_hex: Option<String>,
/// Use the content of FILE as the Configuration Unique ID.
///
/// The file must contain exactly 128 bit of data or a yaml with a `cuid` entry.
/// If specified, the value must match the Config-UID from the attestation result of that
/// guest. If not specified, the CUID will be ignored by the Ultravisor during the verification
/// of the request.
#[arg(long, value_name = "FILE", conflicts_with("cuid_hex"), value_hint = ValueHint::FilePath,)]
pub cuid: Option<String>,
#[command(subcommand)]
pub secret: AddSecretType,
// FLAGS
// each flag must conflict with `flags`
// `flags` is hidden in the help menu
/// Manually set the add-secret request flags.
///
/// No validity checks made. Hidden in user documentation.
#[arg(long, hide(true))]
pub pcf: Option<String>,
/// Flags for the add-secret request.
#[arg(
long,
conflicts_with("pcf"),
value_enum,
value_parser,
use_value_delimiter = true,
value_delimiter = ','
)]
pub flags: Vec<CreateSecretFlags>,
/// Use the content of FILE as user-data.
///
/// Passes user data defined in FILE through the add-secret request to the ultravisor. The
/// user data can be up to 512 bytes of arbitrary data, and the maximum size depends on the
/// size of the user-signing key:
/// - No key: user data can be 512 bytes.
/// - EC(secp521r1) or RSA 2048 keys: user data can be 256 bytes.
/// - RSA 3072 key: user data can be 128 bytes.
///
/// The firmware ignores this data, but the request tag protects the user-data. Optional. No
/// user-data by default.
#[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath,)]
pub user_data: Option<String>,
/// Links an AddSecret-Request (ASR) to a policy file.
///
/// This option embeds a PolicyReference in the ASR user data field. The PolicyReference
/// includes the relative file path and the SHA512 hash of the policy file, allowing the
/// policys integrity to be verified.
///
/// This option conflicts with --user-data, because both options use the same user data field in
/// the ASR structure.
#[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath, conflicts_with("user_data"))]
pub policy: Option<String>,
/// Adds the AESGCM authentication tag to a TOC policy file.
///
/// This option appends the AESGCM authentication tag to the specified TOC policy file. This
/// allows the TOC policy to maintain a list of all ASR MAC tags for completeness verification
/// during boot. During verification, the TOC checks the MAC tags against this list to ensure
/// that all expected ASRs are present and unmodified.
#[arg(
long = "toc-policy",
value_name = "FILE",
value_hint = ValueHint::FilePath,
)]
pub tocpolicy: Option<String>,
/// Use the content of FILE as user signing key.
///
/// Adds a signature calculated from the key in FILE to the add-secret request. The
/// file must be in DER or PEM format containing a private key. Supported are RSA 2048 &
/// 3072-bit and EC(secp521r1) keys. The firmware ignores the content, but the request tag
/// protects the signature. The user-signing key signs the request. The location of the
/// signature is filled with zeros during the signature calculation. The request tag also
/// secures the signature. See man pvsecret verify for more details. Optional. No signature
/// by default.
#[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath,)]
pub user_sign_key: Option<String>,
/// Do not hash the name, use it directly as secret ID.
///
/// Ignored for meta-secrets.
#[arg(long)]
pub use_name: bool,
}
#[derive(Subcommand, Debug)]
pub enum AddSecretType {
/// Create a meta secret.
///
/// Use a meta secret to carry flags to the ultravisor without having to provide an actual
/// secret value. Meta secrets do not appear in the list of secrets.
Meta,
/// Create an association secret.
///
/// Use an association secret to connect a trusted I/O device to a guest. The 'pvapconfig' tool
/// provides more information about association secrets.
Association {
/// String that identifies the new secret.
///
/// The actual secret is set with '--input-secret'. The name is saved in `NAME.yaml` with
/// white-spaces mapped to `_`.
name: String,
/// Print the hashed name to stdout.
///
/// The hashed name is not written to `NAME.yaml`
#[arg(long)]
stdout: bool,
/// Path from which to read the plaintext secret. Uses a random secret if not specified.
#[arg(long, value_name = "SECRET-FILE", value_hint = ValueHint::FilePath, conflicts_with("output_secret"))]
input_secret: Option<String>,
/// Save the generated secret as plaintext in SECRET-FILE.
///
/// The generated secret can be used to generate add-secret requests for a different guest
/// with the same secret using '--input-secret'. Destroy the secret when it is not used
/// anymore.
#[arg(long, value_name = "SECRET-FILE", value_hint = ValueHint::FilePath,)]
output_secret: Option<String>,
},
/// Create a retrievable secret.
///
/// A retrievable secret is stored in the per-guest storage of the Ultravisor. A SE-guest can
/// retrieve the secret at runtime and use it. All retrievable secrets, but the plaintext
/// secret, are retrieved as wrapped/protected key objects and only usable inside the current,
/// running SE-guest instance.
#[command(visible_alias = "retr")]
Retrievable {
/// String that identifies the new secret.
///
/// The actual secret is set with '--secret'. The name is saved in `NAME.yaml` with
/// white-spaces mapped to `_`.
name: String,
/// Print the hashed name to stdout.
///
/// The hashed name is not written to `NAME.yaml`
#[arg(long)]
stdout: bool,
/// Use SECRET-FILE as retrievable secret
#[arg(long, value_name = "SECRET-FILE", value_hint = ValueHint::FilePath)]
secret: String,
/// Specify the secret type.
///
/// Limitations to the input data apply depending on the secret type.
#[arg(long = "type", value_name = "TYPE")]
kind: RetrieveableSecretInpKind,
},
/// Update customer communication key.
///
/// Insert a customer communication key into a guest.
#[command(visible_alias = "cck")]
UpdateCck {
/// Use CCK-FILE as new CCK.
#[arg(long, value_name = "CCK-FILE", value_hint = ValueHint::FilePath)]
secret: String,
},
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum RetrieveableSecretInpKind {
/// A plaintext secret.
/// Can be any file up to 8190 bytes long
Plain,
/// An AES key.
/// Must be a plain byte file 128, 192, or 256 bit long.
Aes,
/// An AES-XTS key.
/// Must be a plain byte file 256, or 512 bit long.
AesXts,
/// A HMAC-SHA key.
/// Must be a plain byte file 512, or 1024 bit long. Special care is required when creating
/// HMAC-SHA keys. For more Information refer to the DESCRIPTION section of the man file.
HmacSha,
/// An elliptic curve private key.
/// Must be a PEM or DER file.
Ec,
}
impl Display for RetrieveableSecretInpKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Plain => "PLAINTEXT",
Self::Aes => "AES KEY",
Self::AesXts => "AES-XTS KEY",
Self::HmacSha => "HMAC-SHA KEY",
Self::Ec => "EC PRIVATE KEY",
}
)
}
}
#[derive(Args, Debug)]
pub struct AddSecretOpt {
/// Specify the request to be sent.
#[arg(value_name = "FILE", value_hint = ValueHint::FilePath,)]
pub input: String,
/// Force the addition of add-secret requests.
///
/// Add an add-secret request even if there is already a secret with the same ID in the secret
/// store.
#[arg(short, long)]
pub force: bool,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Default)]
pub enum ListSecretOutputType {
/// Human-focused, non-parsable output format
#[default]
Human,
/// Use yaml format.
Yaml,
/// Use the format the ultravisor uses to pass the list.
Bin,
}
#[derive(Args, Debug)]
pub struct ListSecretOpt {
/// Store the result in FILE
#[arg(value_name = "FILE", default_value = STDOUT, value_hint = ValueHint::FilePath,)]
pub output: String,
/// Define the output format of the list.
#[arg(long, value_enum, default_value_t)]
pub format: ListSecretOutputType,
}
#[derive(Args, Debug)]
pub struct VerifyOpt {
/// Specify the request to be checked.
#[arg(value_name = "FILE", value_hint = ValueHint::FilePath,)]
pub input: String,
/// Certificate containing a public key used to verify the user data signature.
///
/// Specifies a public key used to verify the user-data signature. The file must be a X509
/// certificate in DSA or PEM format. The certificate must hold the public EC, RSA 2048, or RSA
/// 3072 key corresponding to the private user-key used during `create`. No chain of trust is
/// established. Ensuring that the certificate can be trusted is the responsibility of the
/// user. The EC key must use the NIST/SECG curve over a 521 bit prime field (secp521r1).
#[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath,)]
pub user_cert: Option<String>,
/// Store the result in FILE
///
/// If the request contained abirtary user-data the output contains this user-data with padded
/// zeros if available.
#[arg(short, long, value_name = "FILE", default_value = STDOUT, value_hint = ValueHint::FilePath,)]
pub output: String,
}
#[derive(Args, Debug)]
pub struct RetrSecretOptions {
/// Specify the secret ID to be retrieved.
///
/// Input type depends on '--inform'. If `yaml` (default) is specified, it must be a yaml
/// created by the create subcommand of this tool. If `hex` is specified, it must be a 32 byte
/// handle encodes in hexadecimal. Leading zeros are required. If there are multiple secrets in
/// the store with the same Id there are no guarantees on which specific secret is retrieved.
/// Use --inform=idx to make sure a specific secret is retrieved.
#[arg(value_name = "ID", value_hint = ValueHint::FilePath)]
pub input: String,
/// Specify the output path to place the secret value
#[arg(short, long, value_name = "FILE", default_value = STDOUT, value_hint = ValueHint::FilePath)]
pub output: String,
/// Define input type for the Secret ID
#[arg(long, value_enum, default_value_t)]
pub inform: RetrInpFmt,
/// Define the output format for the retrieved secret
#[arg(long, value_enum, default_value_t)]
pub outform: RetrOutFmt,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Default)]
pub enum RetrInpFmt {
/// Use a yaml file
#[default]
Yaml,
/// Use a hex string.
Hex,
/// Use a name-string. Will hash it if no secret with the name found.
Name,
/// Use the secret-index (base 10) instead of the secret-ID
Idx,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Default)]
pub enum RetrOutFmt {
/// Write the secret as PEM.
///
/// File starts with `-----BEGIN IBM PROTECTED KEY----` and `-----BEGIN
/// PLAINTEXT SECRET-----` for plaintext secrets it contains one header
/// line with the type information and the base64 protected key
#[default]
Pem,
/// Write the secret in binary.
Bin,
}
#[derive(Subcommand, Debug)]
pub enum Command {
/// Create a new add-secret request.
///
/// Create add-secret requests for IBM Secure Execution guests. Only create these requests in a
/// trusted environment, such as your workstation. The 'pvattest create' command creates a
/// randomly generated key to protect the request. The generated requests can then be added on
/// an IBM Secure Execution guest using 'pvsecret add'. The guest can then use the secrets with
/// the use case depending on the secret type.
Create(Box<CreateSecretOpt>),
/// Submit an add-secret request to the Ultravisor (s390x only).
///
/// Perform an add-secret request using a previously generated add-secret request. Only
/// available on s390x.
Add(AddSecretOpt),
/// Lock the secret-store (s390x only).
///
/// Lock the secret store (s390x only). After this command executed successfully, all
/// subsequent add-secret requests will fail. Only available on s390x.
Lock,
/// List all ultravisor secrets (s390x only).
///
/// Lists the IDs of all non-null secrets currently stored in the ultravisor for the currently
/// running IBM Secure Execution guest. Only available on s390x.
List(ListSecretOpt),
/// Verify that an add-secret request is sane.
///
/// Verifies that the given request is an add-secret request by testing for some values to be
/// present. If the request contains signed user-data, the signature is verified with the
/// provided key. Outputs the arbitrary user-data.
Verify(VerifyOpt),
/// Retrieve a secret from the UV secret store (s390x only).
#[command(visible_alias = "retr")]
Retrieve(RetrSecretOptions),
/// Print version information and exit.
#[command(aliases(["--version"]), hide(true))]
Version,
}
/// Additional checks to assure, option integrity
pub fn validate_cli(cli: &CliOptions) -> Result<(), clap::Error> {
if let Command::Create(opt) = &cli.cmd {
if let AddSecretType::Association {
name,
stdout,
input_secret: _,
output_secret: secret_out,
} = &opt.secret
{
if *stdout {
return Ok(());
}
if secret_out == &Some(format!("{name}.yaml")) {
return Err(CliOptions::command().error(
ValueValidation,
format!("Secret output file and the secret name '{name}.yaml' are the same."),
));
}
if format!("{name}.yaml") == opt.output {
return Err(CliOptions::command().error(
ValueValidation,
format!(
"output file and the secret name '{}' are the same.",
&opt.output
),
));
}
}
}
Ok(())
}
#[cfg(test)]
mod test {
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!["pvsecret", "lock"],
vec!["pvsecret", "version"],
vec!["pvsecret", "list"],
vec!["pvsecret", "add", "abc"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "--no-verify", "meta"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "--no-verify", "association", "name" ],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "--no-verify", "update-cck", "--secret", "abc"],
// verify that arguments stay backwards compatible
vec!["pvsecret", "create", "-k", "abc,cdef", "--hdr", "abc", "-o", "abc", "-C", "uuu,ggg", "--crl", "yyy,hhh", "--root-ca", "tttt",
"--extension-secret", "fff", "--cuid", "cuid", "--flags", "disable-dump", "meta"],
vec!["pvsecret", "create", "--host-key-document", "abc", "-k", "y", "--hdr", "abc", "-o", "abc", "--cert", "uuu", "--crl", "yyy",
"--root-ca", "tttt", "--cck", "cck", "--cuid-hex", "0x11223344556677889900aabbccddeeff", "--pcf", "0x123", "association", "name", "--stdout",
"--output-secret", "secret"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "--no-verify", "association", "name", "--output-secret", "secret"],
vec!["pvsecret", "list", "--format", "human"],
vec!["pvsecret", "list", "--format", "yaml"],
vec!["pvsecret", "list", "--format", "bin"],
];
// Test for the minimal amount of flags to yield an invalid combination
let invalid_args = [
vec!["pvsecret"],
vec!["pvsecret", "list", "--yaml", "--bin"],
vec!["pvsecret", "create", "--hdr", "abc", "-o", "abc", "--no-verify" ,"null"],
vec!["pvsecret", "create", "-k", "abc", "-o", "abc", "--no-verify", "null"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "--no-verify", "null"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "null"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "--cck", "abc", "--extension_secret", "abc", "--no-verify", "null"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "--no-verify", "--flags", "disable-dump", "--pcf", "0", "null"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "--no-verify", "--cuid", "abc", "--cuid_hex", "9", "null"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "--no-verify", "association"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "--no-verify", "association", "name", "--output-secret", "secret", "--input-secret", "secret"],
vec!["pvsecret", "create", "-k", "abc", "--hdr", "abc", "-o", "abc", "--no-verify", "update-cck"],
];
for arg in valid_args {
let res = CliOptions::try_parse_from(&arg);
if let Err(e) = &res {
println!("arg: {arg:?}");
println!("{e}");
}
assert!(res.is_ok());
}
for arg in invalid_args {
let res = CliOptions::try_parse_from(&arg);
assert!(res.is_err());
}
}
#[test]
fn verify_cli() {
use clap::CommandFactory;
CliOptions::command().debug_assert()
}
}