rust/pvsecret: Add support for retrievable secrets

Support for creating and retrieving retrievable secrets.

Acked-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Christoph Schlameuss <schlameuss@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
This commit is contained in:
Steffen Eiden
2024-02-19 15:15:16 +01:00
committed by Jan Höppner
parent a14f9d4edc
commit 93da795520
6 changed files with 230 additions and 10 deletions

View File

@@ -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<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,
},
}
#[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

View File

@@ -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] = &[];
}

View File

@@ -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<P, D>(path: &P, data: D, ctx: &str) -> pv::Result<()>
where
P: AsRef<Path>,
@@ -32,6 +33,23 @@ where
Ok(())
}
fn retrievable(name: &str, secret: &str, kind: &RetrieveableSecretInpKind) -> Result<GuestSecret> {
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<AddSecretRequest> {
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<AddSecretRequest> {
.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<P: AsRef<Path>>(
write_out(path, guest_secret.confidential(), "Association secret")?
}
}
AddSecretType::Retrievable { name, stdout, .. } => {
write_yaml(name, guest_secret, stdout, outp_path)?
}
_ => (),
};
Ok(())

View File

@@ -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<SecretList> {
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 {

View File

@@ -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<RetrievedSecret> {
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(())
}

View File

@@ -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 {