From 93da795520ca2f0a73cfbfc951a9b16437a1b95b Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Mon, 19 Feb 2024 15:15:16 +0100 Subject: [PATCH] rust/pvsecret: Add support for retrievable secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support for creating and retrieving retrievable secrets. Acked-by: Marc Hartmayer Reviewed-by: Christoph Schlameuss Signed-off-by: Steffen Eiden Signed-off-by: Jan Höppner --- rust/pvsecret/src/cli.rs | 129 +++++++++++++++++++++++++++++++- rust/pvsecret/src/cmd.rs | 6 +- rust/pvsecret/src/cmd/create.rs | 30 +++++++- rust/pvsecret/src/cmd/list.rs | 12 ++- rust/pvsecret/src/cmd/retr.rs | 62 +++++++++++++++ rust/pvsecret/src/main.rs | 1 + 6 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 rust/pvsecret/src/cmd/retr.rs diff --git a/rust/pvsecret/src/cli.rs b/rust/pvsecret/src/cli.rs index c4b9f2b3..4e747682 100644 --- a/rust/pvsecret/src/cli.rs +++ b/rust/pvsecret/src/cli.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT // -// Copyright IBM Corp. 2023 +// 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}; @@ -177,6 +180,72 @@ pub enum AddSecretType { #[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, + }, +} + +#[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 512, or 1024 bit long. + AesXts, + /// A HMAC-SHA key. + /// Must be a plain byte file 512, or 1024 bit long. + 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", + } + ) + } } // all members s390x only @@ -238,6 +307,56 @@ pub struct VerifyOpt { pub output: String, } +// all members s390x only +#[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 hex + /// 32-byte unsigned big endian number string. Leading zeros are required. + #[cfg(target_arch = "s390x")] + #[arg(value_name = "ID", value_hint = ValueHint::FilePath)] + pub input: String, + + /// Specify the output path to place the secret value + #[cfg(target_arch = "s390x")] + #[arg(short, long, value_name = "FILE", default_value = STDOUT, value_hint = ValueHint::FilePath)] + pub output: String, + + /// Define input type for the Secret ID + #[cfg(target_arch = "s390x")] + #[arg(long, value_enum, default_value_t)] + pub inform: RetrInpFmt, + + /// Define the output format for the retrieved secret + #[cfg(target_arch = "s390x")] + #[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, +} + +#[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. @@ -274,6 +393,10 @@ pub enum Command { /// 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, @@ -294,13 +417,13 @@ pub fn validate_cli(cli: &CliOptions) -> Result<(), clap::Error> { } if secret_out == &Some(format!("{name}.yaml")) { return Err(CliOptions::command().error( - clap::error::ErrorKind::ValueValidation, + 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( - clap::error::ErrorKind::ValueValidation, + ValueValidation, format!( "output file and the secret name '{}' are the same.", &opt.output diff --git a/rust/pvsecret/src/cmd.rs b/rust/pvsecret/src/cmd.rs index a826fb31..10d99a5b 100644 --- a/rust/pvsecret/src/cmd.rs +++ b/rust/pvsecret/src/cmd.rs @@ -16,6 +16,8 @@ mod add; mod list; #[cfg(target_arch = "s390x")] mod lock; +#[cfg(target_arch = "s390x")] +mod retr; // Commands (directly) related to UVCs are only available on s389x #[cfg(target_arch = "s390x")] @@ -24,12 +26,13 @@ mod uv_cmd { pub use add::add; pub use list::list; pub use lock::lock; + pub use retr::retr; pub const UV_CMD_FN: &[&str] = &["+add", "+lock", "+list"]; } #[cfg(not(target_arch = "s390x"))] mod uv_cmd { - use crate::cli::{AddSecretOpt, ListSecretOpt}; + use crate::cli::{AddSecretOpt, ListSecretOpt, RetrSecretOptions}; use anyhow::{bail, Result}; macro_rules! not_supp { ($name: ident $( ,$opt: ty )?) => { @@ -40,6 +43,7 @@ mod uv_cmd { } not_supp!(add, AddSecretOpt); not_supp!(list, ListSecretOpt); + not_supp!(retr, RetrSecretOptions); not_supp!(lock); pub const UV_CMD_FN: &[&str] = &[]; } diff --git a/rust/pvsecret/src/cmd/create.rs b/rust/pvsecret/src/cmd/create.rs index 9251c38c..73089a12 100644 --- a/rust/pvsecret/src/cmd/create.rs +++ b/rust/pvsecret/src/cmd/create.rs @@ -4,7 +4,6 @@ use std::path::Path; -use crate::cli::{AddSecretType, CreateSecretFlags, CreateSecretOpt}; use anyhow::{anyhow, bail, Context, Error, Result}; use log::{debug, info, trace, warn}; use pv::{ @@ -22,6 +21,8 @@ use pv::{ use serde_yaml::Value; use utils::get_writer_from_cli_file_arg; +use crate::cli::{AddSecretType, CreateSecretFlags, CreateSecretOpt, RetrieveableSecretInpKind}; + fn write_out(path: &P, data: D, ctx: &str) -> pv::Result<()> where P: AsRef, @@ -32,6 +33,23 @@ where Ok(()) } +fn retrievable(name: &str, secret: &str, kind: &RetrieveableSecretInpKind) -> Result { + let secret_data = read_file(secret, &format!("retrievable {kind}"))?.into(); + + match kind { + RetrieveableSecretInpKind::Plain => GuestSecret::plaintext(name, secret_data), + RetrieveableSecretInpKind::Aes => GuestSecret::aes(name, secret_data), + RetrieveableSecretInpKind::AesXts => GuestSecret::aes_xts(name, secret_data), + RetrieveableSecretInpKind::HmacSha => GuestSecret::hmac_sha(name, secret_data), + RetrieveableSecretInpKind::Ec => GuestSecret::ec( + name, + read_private_key(secret_data.value()) + .with_context(|| format!("Cannot read {secret} as {kind} from PEM or DER"))?, + ), + } + .map_err(Error::from) +} + /// Prepare an add-secret request pub fn create(opt: &CreateSecretOpt) -> Result<()> { if pv_guest_bit_set() { @@ -88,6 +106,9 @@ fn build_asrcb(opt: &CreateSecretOpt) -> Result { input_secret: None, .. } => GuestSecret::association(name, None)?, + AddSecretType::Retrievable { + name, secret, kind, .. + } => retrievable(name, secret, kind)?, }; trace!("AddSecret: {secret:x?}"); @@ -136,7 +157,9 @@ fn build_asrcb(opt: &CreateSecretOpt) -> Result { .as_ref() .map(|p| read_file(p, "User-signing key")) .transpose()? - .map(|buf| read_private_key(&buf)) + .map(|buf| { + read_private_key(&buf).context("Cannot read {secret} as private key from PEM or DER") + }) .transpose()?; if user_data.is_some() || user_key.is_some() { @@ -258,6 +281,9 @@ fn write_secret>( write_out(path, guest_secret.confidential(), "Association secret")? } } + AddSecretType::Retrievable { name, stdout, .. } => { + write_yaml(name, guest_secret, stdout, outp_path)? + } _ => (), }; Ok(()) diff --git a/rust/pvsecret/src/cmd/list.rs b/rust/pvsecret/src/cmd/list.rs index f7e3a72b..0bd9eca4 100644 --- a/rust/pvsecret/src/cmd/list.rs +++ b/rust/pvsecret/src/cmd/list.rs @@ -3,21 +3,25 @@ // Copyright IBM Corp. 2023 use crate::cli::{ListSecretOpt, ListSecretOutputType}; -use anyhow::{Context, Result}; +use anyhow::{Context, Error, Result}; use log::warn; use pv::uv::{ListCmd, SecretList, UvDevice, UvcSuccess}; use utils::{get_writer_from_cli_file_arg, STDOUT}; /// Do a List Secrets UVC -pub fn list(opt: &ListSecretOpt) -> Result<()> { - let uv = UvDevice::open()?; +pub fn list_uvc(uv: &UvDevice) -> Result { let mut cmd = ListCmd::default(); match uv.send_cmd(&mut cmd)? { UvcSuccess::RC_SUCCESS => (), UvcSuccess::RC_MORE_DATA => warn!("There is more data available than expected"), }; + cmd.try_into().map_err(Error::new) +} - let secret_list: SecretList = cmd.try_into()?; +/// Do a List Secrets UVC and output the list in the requested format +pub fn list(opt: &ListSecretOpt) -> Result<()> { + let uv = UvDevice::open()?; + let secret_list = list_uvc(&uv)?; let mut wr_out = get_writer_from_cli_file_arg(&opt.output)?; match &opt.format { diff --git a/rust/pvsecret/src/cmd/retr.rs b/rust/pvsecret/src/cmd/retr.rs new file mode 100644 index 00000000..7f7704cc --- /dev/null +++ b/rust/pvsecret/src/cmd/retr.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 + +use super::list::list_uvc; +use crate::cli::{RetrInpFmt, RetrOutFmt, RetrSecretOptions}; +use anyhow::{anyhow, bail, Context, Result}; +use log::{debug, info}; +use pv::{ + misc::open_file, + misc::write, + secret::{GuestSecret, RetrievedSecret}, + uv::{RetrieveCmd, SecretId, UvDevice}, +}; +use utils::get_writer_from_cli_file_arg; + +fn retrieve(id: &SecretId) -> Result { + let uv = UvDevice::open()?; + let secrets = list_uvc(&uv)?; + let secret = secrets + .into_iter() + .find(|s| s.id() == id.as_ref()) + .ok_or(anyhow!( + "The UV secret-store has no secret with the ID {id}" + ))?; + + info!("Try to retrieve secret at index: {}", secret.index()); + debug!("Try to retrieve: {secret:?}"); + + let mut uv_cmd = RetrieveCmd::from_entry(secret)?; + uv.send_cmd(&mut uv_cmd)?; + + Ok(RetrievedSecret::from_cmd(uv_cmd)) +} + +pub fn retr(opt: &RetrSecretOptions) -> Result<()> { + let mut output = get_writer_from_cli_file_arg(&opt.output)?; + let id = match &opt.inform { + RetrInpFmt::Yaml => match serde_yaml::from_reader(&mut open_file(&opt.input)?)? { + GuestSecret::Retrievable { id, .. } => id, + gs => bail!("The file contains a {gs}-secret, which is not retrievable."), + }, + RetrInpFmt::Hex => { + serde_yaml::from_str(&opt.input).context("Cannot parse SecretId information")? + } + }; + + let retr_secret = + retrieve(&id).context("Could not retrieve the secret from the UV secret store.")?; + + let out_data = match opt.outform { + RetrOutFmt::Bin => retr_secret.into_bytes(), + RetrOutFmt::Pem => retr_secret.to_pem()?.into_bytes(), + }; + write( + &mut output, + out_data.value(), + &opt.output, + "IBM Protected Key", + )?; + Ok(()) +} diff --git a/rust/pvsecret/src/main.rs b/rust/pvsecret/src/main.rs index 502a6ea0..883a3ee2 100644 --- a/rust/pvsecret/src/main.rs +++ b/rust/pvsecret/src/main.rs @@ -45,6 +45,7 @@ fn main() -> ExitCode { Command::Create(opt) => cmd::create(opt), Command::Version => Ok(print_version!("2024", log_level; FEATURES.concat())), Command::Verify(opt) => cmd::verify(opt), + Command::Retrieve(opt) => cmd::retr(opt), }; match res {