// 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, /// 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, /// 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, /// 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, #[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, /// Flags for the add-secret request. #[arg( long, conflicts_with("pcf"), value_enum, value_parser, use_value_delimiter = true, value_delimiter = ',' )] pub flags: Vec, /// 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, /// Links an Add‑Secret-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 SHA‑512 hash of the policy file, allowing the /// policy’s 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, /// Adds the AES‑GCM authentication tag to a TOC policy file. /// /// This option appends the AES‑GCM 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, /// 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, /// 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, /// 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, }, /// 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, /// 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), /// 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() } }