mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
rust/pvattest: Add check command
Add a new command: check. This allows users to perform policy checks on the Attestation result. The host-key hashes, and the user-data can be tested for certain values. While at it fix some typos and enable CSV parsing for the Additional-data flags. Example: ``` pvattest check attestresp checkresult -k hkd0.crt,hkd1.crt --host-key-check AttKeyCheck ``` Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com> Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
This commit is contained in:
@@ -139,6 +139,19 @@ impl<'a, T: Serialize + From<&'a [u8]> + Sized> AdditionalData<T> {
|
|||||||
unrecognized: unrecognized.map(|i| i.into()),
|
unrecognized: unrecognized.map(|i| i.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create from a slice of additional-data
|
||||||
|
///
|
||||||
|
/// `data`: Unstructured additional-data
|
||||||
|
/// `flags`: Flags indicating which additional-data field is present.
|
||||||
|
///
|
||||||
|
/// # Error
|
||||||
|
///
|
||||||
|
/// Fails if there is a mismatch between the data and the flags. Should not happen after a
|
||||||
|
/// successful attestation verification.
|
||||||
|
pub fn from_slice_sized(data: &'a [u8], flags: &AttestationFlags) -> Result<Self> {
|
||||||
|
AdditionalData::<&'a [u8]>::from_slice(data, flags).map(Self::from_other)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> AdditionalData<&'a [u8]> {
|
impl<'a> AdditionalData<&'a [u8]> {
|
||||||
|
|||||||
59
rust/pvattest/src/additional.rs
Normal file
59
rust/pvattest/src/additional.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Copyright IBM Corp. 2024
|
||||||
|
|
||||||
|
use crate::exchange::ExchangeFormatResponse;
|
||||||
|
use anyhow::Result;
|
||||||
|
use pv::attest::{AdditionalData, AttestationFlags};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use utils::HexSlice;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct AttestationResult<'a> {
|
||||||
|
pub cuid: HexSlice<'a>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub add: Option<HexSlice<'a>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub add_fields: Option<AdditionalData<HexSlice<'a>>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub user_data: Option<HexSlice<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AttestationResult<'a> {
|
||||||
|
pub fn from_exchange(
|
||||||
|
resp: &'a ExchangeFormatResponse,
|
||||||
|
flags: &AttestationFlags,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let add_fields = resp
|
||||||
|
.additional()
|
||||||
|
.map(|a| AdditionalData::from_slice_sized(a, flags))
|
||||||
|
.transpose()?;
|
||||||
|
Ok(Self {
|
||||||
|
cuid: resp.config_uid().into(),
|
||||||
|
add: resp.additional().map(|a| a.into()),
|
||||||
|
add_fields,
|
||||||
|
user_data: resp.user().map(|u| u.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AttestationResult<'_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
writeln!(f, "Config UID:")?;
|
||||||
|
writeln!(f, "{:#}", self.cuid)?;
|
||||||
|
if let Some(data) = &self.add {
|
||||||
|
writeln!(f, "Additional-data:")?;
|
||||||
|
writeln!(f, "{:#}", data)?;
|
||||||
|
}
|
||||||
|
if let Some(data) = &self.add_fields {
|
||||||
|
writeln!(f, "Additional-data content:")?;
|
||||||
|
writeln!(f, "{:#}", data)?;
|
||||||
|
}
|
||||||
|
if let Some(data) = &self.user_data {
|
||||||
|
writeln!(f, "user-data:")?;
|
||||||
|
writeln!(f, "{:#}", data)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
//
|
//
|
||||||
// Copyright IBM Corp. 2024
|
// Copyright IBM Corp. 2024
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::{Args, Parser, Subcommand, ValueEnum, ValueHint};
|
use clap::{Args, Parser, Subcommand, ValueEnum, ValueHint};
|
||||||
use utils::{CertificateOptions, DeprecatedVerbosityOptions};
|
use utils::{CertificateOptions, DeprecatedVerbosityOptions};
|
||||||
|
|
||||||
@@ -51,6 +53,11 @@ pub enum Command {
|
|||||||
/// Execution header of the image that was attested during ’pvattest perform’
|
/// Execution header of the image that was attested during ’pvattest perform’
|
||||||
Verify(VerifyOpt),
|
Verify(VerifyOpt),
|
||||||
|
|
||||||
|
/// Check if the attestation result matches defined policies.
|
||||||
|
///
|
||||||
|
/// After the attestation verification, check whether the attestation result complies with user-defined policies.
|
||||||
|
Check(CheckOpt),
|
||||||
|
|
||||||
/// Print version information and exit.
|
/// Print version information and exit.
|
||||||
#[command(aliases(["--version"]), hide(true))]
|
#[command(aliases(["--version"]), hide(true))]
|
||||||
Version,
|
Version,
|
||||||
@@ -191,7 +198,7 @@ pub struct VerifyOpt {
|
|||||||
/// Writes the user data, if the response contains any, to FILE
|
/// Writes the user data, if the response contains any, to FILE
|
||||||
/// The user-data is part of the attestation measurement. If the user-data is written to FILE
|
/// The user-data is part of the attestation measurement. If the user-data is written to FILE
|
||||||
/// the user-data was part of the measurement and verified.
|
/// the user-data was part of the measurement and verified.
|
||||||
/// Emits a warning if the response contains no user-data
|
/// Emits a warning if the response contains no user-data.
|
||||||
#[arg(long, short ,value_name = "FILE", value_hint = ValueHint::FilePath,)]
|
#[arg(long, short ,value_name = "FILE", value_hint = ValueHint::FilePath,)]
|
||||||
pub user_data: Option<String>,
|
pub user_data: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -202,3 +209,73 @@ pub enum OutputType {
|
|||||||
#[default]
|
#[default]
|
||||||
Yaml,
|
Yaml,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct CheckOpt {
|
||||||
|
/// Specify the attestation response to check whether the policies are validated.
|
||||||
|
#[arg(value_name = "IN", value_hint = ValueHint::FilePath,)]
|
||||||
|
pub input: PathBuf,
|
||||||
|
|
||||||
|
/// Specify the output file for the check result.
|
||||||
|
#[arg(value_name = "OUT", value_hint = ValueHint::FilePath,)]
|
||||||
|
pub output: PathBuf,
|
||||||
|
|
||||||
|
/// Define the output format.
|
||||||
|
#[arg(long, value_enum, default_value_t)]
|
||||||
|
pub format: OutputType,
|
||||||
|
|
||||||
|
/// Use FILE to check for a host-key document.
|
||||||
|
///
|
||||||
|
/// Verifies that the attestation response contains the host-key hash of one of the specified
|
||||||
|
/// host keys. The check fails if none of the host-keys match the hash in the response. This
|
||||||
|
/// parameter can be specified multiple times.
|
||||||
|
#[arg(
|
||||||
|
short = 'k',
|
||||||
|
long = "host-key-document",
|
||||||
|
value_name = "FILE",
|
||||||
|
value_hint = ValueHint::FilePath,
|
||||||
|
use_value_delimiter = true,
|
||||||
|
value_delimiter = ',',
|
||||||
|
)]
|
||||||
|
pub host_key_documents: Vec<PathBuf>,
|
||||||
|
|
||||||
|
/// Define the host-key check policy
|
||||||
|
///
|
||||||
|
/// By default, all host-key hashes are checked, and it is not considered a failure if a hash
|
||||||
|
/// is missing from the attestation response. Use this policy switch to trigger a failure if no
|
||||||
|
/// corresponding hash is found. Requires at least one host-key document.
|
||||||
|
#[arg(
|
||||||
|
long = "host-key-check",
|
||||||
|
requires("host_key_documents"),
|
||||||
|
use_value_delimiter = true,
|
||||||
|
value_delimiter = ','
|
||||||
|
)]
|
||||||
|
pub host_key_checks: Vec<HostKeyCheckPolicy>,
|
||||||
|
|
||||||
|
/// Check if the provided user data matches the data from the attestation response.
|
||||||
|
#[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath,)]
|
||||||
|
pub user_data: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
|
||||||
|
pub enum HostKeyCheckPolicy {
|
||||||
|
/// Check the host-key used for the attestation request.
|
||||||
|
///
|
||||||
|
/// The check is considered failed, if no given host-key matches the hash from the attestation
|
||||||
|
/// host-key hash, or no attestation host-key has is in the attestation response.
|
||||||
|
AttKeyHash,
|
||||||
|
/// Check the host-key used to the boot the image.
|
||||||
|
///
|
||||||
|
/// The check is considered failed, if no given host-key matches the hash from the boot
|
||||||
|
/// host-key hash, or no boot host-key has is in the attestation response.
|
||||||
|
BootKeyHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
#[test]
|
||||||
|
fn verify_cli() {
|
||||||
|
use clap::CommandFactory;
|
||||||
|
super::CliOptions::command().debug_assert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
//
|
//
|
||||||
// Copyright IBM Corp. 2024
|
// Copyright IBM Corp. 2024
|
||||||
//
|
//
|
||||||
|
pub mod check;
|
||||||
pub mod create;
|
pub mod create;
|
||||||
#[cfg(target_arch = "s390x")]
|
#[cfg(target_arch = "s390x")]
|
||||||
pub mod perform;
|
pub mod perform;
|
||||||
pub mod verify;
|
pub mod verify;
|
||||||
|
|
||||||
|
pub use check::check;
|
||||||
pub use create::create;
|
pub use create::create;
|
||||||
pub use verify::verify;
|
pub use verify::verify;
|
||||||
|
|
||||||
|
|||||||
133
rust/pvattest/src/cmd/check.rs
Normal file
133
rust/pvattest/src/cmd/check.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Copyright IBM Corp. 2024
|
||||||
|
|
||||||
|
mod host_key;
|
||||||
|
|
||||||
|
use self::host_key::{host_key_check, HostKeyCheck};
|
||||||
|
use crate::{additional::AttestationResult, cli::CheckOpt, exchange::ExchangeFormatResponse};
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use pv::{
|
||||||
|
attest::AttestationRequest,
|
||||||
|
misc::{create_file, open_file, read_file},
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
use utils::HexSlice;
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
enum CheckState<T> {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Data(T),
|
||||||
|
Err(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> CheckState<T> {
|
||||||
|
fn check(self, issues: &mut Vec<String>) -> Option<T> {
|
||||||
|
match self {
|
||||||
|
Self::None => None,
|
||||||
|
Self::Data(d) => Some(d),
|
||||||
|
Self::Err(e) => {
|
||||||
|
issues.push(e.to_string());
|
||||||
|
warn!("✘ {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Option<T>> for CheckState<T> {
|
||||||
|
fn from(value: Option<T>) -> Self {
|
||||||
|
value.map_or(Self::None, Self::Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a [`CheckState::Err`]`
|
||||||
|
#[allow(unused_macro_rules)]
|
||||||
|
macro_rules! bail_check {
|
||||||
|
($msg:literal) => {
|
||||||
|
return Ok(CheckState::Err($msg.to_string()))
|
||||||
|
};
|
||||||
|
($err:expr) => {
|
||||||
|
return Ok(CheckState::Err($err.to_string()))
|
||||||
|
};
|
||||||
|
($fmt:expr, $($arg:tt)*) => {
|
||||||
|
return Ok(CheckState::Err(format!($fmt, $($arg)*)))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
use bail_check;
|
||||||
|
|
||||||
|
/// Check if the user-data matches with the user-data in the attestation response
|
||||||
|
fn user_data_check<'a>(
|
||||||
|
opt: &CheckOpt,
|
||||||
|
att_res: &'a AttestationResult,
|
||||||
|
) -> Result<CheckState<HexSlice<'a>>> {
|
||||||
|
let user_data = match &opt.user_data {
|
||||||
|
Some(file) => read_file(file, "user-data")?,
|
||||||
|
None => return Ok(CheckState::None),
|
||||||
|
};
|
||||||
|
|
||||||
|
if Some(HexSlice::from(&user_data)) != att_res.user_data {
|
||||||
|
bail_check!(
|
||||||
|
"The Provided user data does not match the user data from the attestation response."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
info!("✓ Checked user-data");
|
||||||
|
Ok(att_res.user_data.clone().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Default)]
|
||||||
|
pub struct CheckResult<'a> {
|
||||||
|
successful: bool,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
issues: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "HostKeyCheck::hide")]
|
||||||
|
image_host_key: HostKeyCheck<'a>,
|
||||||
|
#[serde(skip_serializing_if = "HostKeyCheck::hide")]
|
||||||
|
attest_host_key: HostKeyCheck<'a>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
user_data: Option<HexSlice<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform the policy checks
|
||||||
|
pub fn check(opt: &CheckOpt) -> Result<ExitCode> {
|
||||||
|
let mut input = open_file(&opt.input)?;
|
||||||
|
let inp = ExchangeFormatResponse::read(&mut input)?;
|
||||||
|
let auth = AttestationRequest::auth_bin(inp.arcb())?;
|
||||||
|
let att_res = AttestationResult::from_exchange(&inp, auth.flags())?;
|
||||||
|
let mut issues = vec![];
|
||||||
|
|
||||||
|
let image_host_key = host_key_check(opt, host_key::HkCheck::Image, &att_res)?
|
||||||
|
.check(&mut issues)
|
||||||
|
.unwrap();
|
||||||
|
let attest_host_key = host_key_check(opt, host_key::HkCheck::Attest, &att_res)?
|
||||||
|
.check(&mut issues)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let user_data = user_data_check(opt, &att_res)?.check(&mut issues);
|
||||||
|
|
||||||
|
let res = CheckResult {
|
||||||
|
successful: !issues.is_empty(),
|
||||||
|
issues,
|
||||||
|
image_host_key,
|
||||||
|
attest_host_key,
|
||||||
|
user_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("res {res:?}");
|
||||||
|
let output = create_file(&opt.output)?;
|
||||||
|
serde_yaml::to_writer(output, &res)?;
|
||||||
|
|
||||||
|
match res.successful {
|
||||||
|
true => {
|
||||||
|
warn!("✓ The Attestation response fulfills all policies");
|
||||||
|
Ok(ExitCode::SUCCESS)
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
warn!("✘ The Attestation response does not fulfill all policies");
|
||||||
|
Ok(ExitCode::from(crate::EXIT_CODE_ATTESTATION_FAIL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
176
rust/pvattest/src/cmd/check/host_key.rs
Normal file
176
rust/pvattest/src/cmd/check/host_key.rs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Copyright IBM Corp. 2024
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::{debug, info};
|
||||||
|
use pv::{
|
||||||
|
misc::{read_certs, read_file},
|
||||||
|
request::{openssl::DigestBytes, EcPubKeyCoord},
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::{fmt::Display, path::Path};
|
||||||
|
use utils::HexSlice;
|
||||||
|
|
||||||
|
use super::CheckState;
|
||||||
|
use crate::{
|
||||||
|
additional::AttestationResult,
|
||||||
|
cli::{CheckOpt, HostKeyCheckPolicy},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum HkCheck {
|
||||||
|
Image,
|
||||||
|
Attest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for HkCheck {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} public host-key hash",
|
||||||
|
match self {
|
||||||
|
Self::Image => "image",
|
||||||
|
Self::Attest => "attestation",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_host_keys<A: AsRef<Path>>(hkds: &[A]) -> Result<Vec<(&Path, DigestBytes)>> {
|
||||||
|
let mut hkd_hash = Vec::with_capacity(hkds.len());
|
||||||
|
for hkd in hkds {
|
||||||
|
let hkd = hkd.as_ref();
|
||||||
|
let hk = read_file(hkd, "host-key document")?;
|
||||||
|
let certs = read_certs(&hk).map_err(|source| pv::Error::HkdNotPemOrDer {
|
||||||
|
hkd: hkd.display().to_string(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
let ec_coord: EcPubKeyCoord = certs.first().unwrap().public_key()?.as_ref().try_into()?;
|
||||||
|
hkd_hash.push((hkd, ec_coord.sha256()?));
|
||||||
|
}
|
||||||
|
Ok(hkd_hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_phkh<'a>(
|
||||||
|
hkd_hashes: &[(&'a Path, DigestBytes)],
|
||||||
|
phkh: &HexSlice<'_>,
|
||||||
|
mode: HkCheck,
|
||||||
|
check_enforced: bool,
|
||||||
|
) -> CheckState<HostKeyCheck<'a>> {
|
||||||
|
let hk: Vec<_> = hkd_hashes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(path, hash)| match hash.as_ref() == phkh.as_ref() {
|
||||||
|
true => Some(*path),
|
||||||
|
false => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
debug!("HK: {hk:?}");
|
||||||
|
match hk.len() {
|
||||||
|
0 => CheckState::Err(format!(
|
||||||
|
"No given host-key document matches the given {mode}"
|
||||||
|
)),
|
||||||
|
1 => CheckState::Data(HostKeyCheck::new(check_enforced, hk.first().copied())),
|
||||||
|
_ => CheckState::Err(format!(
|
||||||
|
"More than one host-key document matches the given {mode}"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Default)]
|
||||||
|
pub struct HostKeyCheck<'a> {
|
||||||
|
check_enforced: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
hash: Option<&'a Path>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> HostKeyCheck<'a> {
|
||||||
|
pub fn new(check_enforced: bool, hash: Option<&'a Path>) -> Self {
|
||||||
|
Self {
|
||||||
|
check_enforced,
|
||||||
|
hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide(&self) -> bool {
|
||||||
|
self.hash.is_none() && !self.check_enforced
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn host_key_check<'a, 'b>(
|
||||||
|
opt: &'a CheckOpt,
|
||||||
|
kind: HkCheck,
|
||||||
|
att_res: &'b AttestationResult<'b>,
|
||||||
|
) -> Result<CheckState<HostKeyCheck<'a>>> {
|
||||||
|
if opt.host_key_documents.is_empty() {
|
||||||
|
return Ok(CheckState::Data(HostKeyCheck::default()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let check_enforced = opt.host_key_checks.contains(&match kind {
|
||||||
|
HkCheck::Image => HostKeyCheckPolicy::BootKeyHash,
|
||||||
|
HkCheck::Attest => HostKeyCheckPolicy::AttKeyHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
let hkd_hashes = load_host_keys(&opt.host_key_documents)?;
|
||||||
|
|
||||||
|
let res = match att_res
|
||||||
|
.add_fields
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|add_fields| match kind {
|
||||||
|
HkCheck::Image => add_fields.image_public_host_key_hash(),
|
||||||
|
HkCheck::Attest => add_fields.attestation_public_host_key_hash(),
|
||||||
|
}) {
|
||||||
|
Some(phkh) => contains_phkh(&hkd_hashes, phkh, kind, check_enforced),
|
||||||
|
None if check_enforced => CheckState::Err(format!(
|
||||||
|
"The Attestation result does not contain an {}, but checking was enabled.",
|
||||||
|
kind
|
||||||
|
)),
|
||||||
|
None => CheckState::Data(HostKeyCheck::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("✓ Check {kind}");
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_hash_neq() {
|
||||||
|
let hostkey =
|
||||||
|
[concat!(env!("CARGO_MANIFEST_DIR"), "/tests/assets/host.pem.crt").to_string()];
|
||||||
|
let hash = load_host_keys(&hostkey).unwrap();
|
||||||
|
|
||||||
|
let res = contains_phkh(&hash, &HexSlice::from(&[0; 32]), HkCheck::Image, true);
|
||||||
|
assert!(matches!(res, CheckState::Err(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_hash_mul() {
|
||||||
|
let hostkey = [
|
||||||
|
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/assets/host.pem.crt").to_string(),
|
||||||
|
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/assets/host.pem.crt").to_string(),
|
||||||
|
];
|
||||||
|
let hash = load_host_keys(&hostkey).unwrap();
|
||||||
|
|
||||||
|
let res = contains_phkh(&hash, &HexSlice::from(&hash[0].1), HkCheck::Image, true);
|
||||||
|
assert!(matches!(res, CheckState::Err(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_hash_eq() {
|
||||||
|
let hostkey =
|
||||||
|
[concat!(env!("CARGO_MANIFEST_DIR"), "/tests/assets/host.pem.crt").to_string()];
|
||||||
|
let hash = load_host_keys(&hostkey).unwrap();
|
||||||
|
|
||||||
|
let res = contains_phkh(&hash, &HexSlice::from(&hash[0].1), HkCheck::Image, true);
|
||||||
|
assert!(matches!(
|
||||||
|
res,
|
||||||
|
CheckState::Data(s) if s.hash.unwrap() == PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/assets/host.pem.crt"))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,71 +5,20 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use pv::{
|
use pv::{
|
||||||
attest::{
|
attest::{AttestationItems, AttestationMeasurement, AttestationRequest},
|
||||||
AdditionalData, AttestationFlags, AttestationItems, AttestationMeasurement,
|
|
||||||
AttestationRequest,
|
|
||||||
},
|
|
||||||
misc::{create_file, open_file, read_exact_file, write_file},
|
misc::{create_file, open_file, read_exact_file, write_file},
|
||||||
request::{openssl::pkey::PKey, BootHdrTags, Confidential, SymKey},
|
request::{openssl::pkey::PKey, BootHdrTags, Confidential, SymKey},
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use std::process::ExitCode;
|
||||||
use std::{fmt::Display, process::ExitCode};
|
|
||||||
use utils::HexSlice;
|
use utils::HexSlice;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
additional::AttestationResult,
|
||||||
cli::{OutputType, VerifyOpt},
|
cli::{OutputType, VerifyOpt},
|
||||||
exchange::ExchangeFormatResponse,
|
exchange::ExchangeFormatResponse,
|
||||||
EXIT_CODE_ATTESTATION_FAIL,
|
EXIT_CODE_ATTESTATION_FAIL,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct VerifyOutput<'a> {
|
|
||||||
cuid: HexSlice<'a>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
add: Option<HexSlice<'a>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
add_fields: Option<AdditionalData<HexSlice<'a>>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
user_data: Option<HexSlice<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> VerifyOutput<'a> {
|
|
||||||
fn from_exchange(resp: &'a ExchangeFormatResponse, flags: &AttestationFlags) -> Result<Self> {
|
|
||||||
let additional_data_fields = resp
|
|
||||||
.additional()
|
|
||||||
.map(|a| AdditionalData::from_slice(a, flags))
|
|
||||||
.transpose()?;
|
|
||||||
let user_data = resp.user().map(|u| u.into());
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
cuid: resp.config_uid().into(),
|
|
||||||
add: resp.additional().map(|a| a.into()),
|
|
||||||
add_fields: additional_data_fields.map(AdditionalData::from_other),
|
|
||||||
user_data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for VerifyOutput<'_> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
writeln!(f, "Config UID:")?;
|
|
||||||
writeln!(f, "{:#}", self.cuid)?;
|
|
||||||
if let Some(data) = &self.add {
|
|
||||||
writeln!(f, "Additional-data:")?;
|
|
||||||
writeln!(f, "{:#}", data)?;
|
|
||||||
}
|
|
||||||
if let Some(data) = &self.add_fields {
|
|
||||||
writeln!(f, "Additional-data content:")?;
|
|
||||||
writeln!(f, "{:#}", data)?;
|
|
||||||
}
|
|
||||||
if let Some(data) = &self.user_data {
|
|
||||||
writeln!(f, "user-data:")?;
|
|
||||||
writeln!(f, "{:#}", data)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify(opt: &VerifyOpt) -> Result<ExitCode> {
|
pub fn verify(opt: &VerifyOpt) -> Result<ExitCode> {
|
||||||
let mut input = open_file(&opt.input)?;
|
let mut input = open_file(&opt.input)?;
|
||||||
let mut img = open_file(&opt.hdr)?;
|
let mut img = open_file(&opt.hdr)?;
|
||||||
@@ -102,7 +51,7 @@ pub fn verify(opt: &VerifyOpt) -> Result<ExitCode> {
|
|||||||
}
|
}
|
||||||
warn!("Attestation measurement verified");
|
warn!("Attestation measurement verified");
|
||||||
// Error impossible CUID is present Attestation verified
|
// Error impossible CUID is present Attestation verified
|
||||||
let pr_data = VerifyOutput::from_exchange(&exchange, auth.flags())?;
|
let pr_data = AttestationResult::from_exchange(&exchange, auth.flags())?;
|
||||||
|
|
||||||
warn!("{pr_data}");
|
warn!("{pr_data}");
|
||||||
if let Some(mut output) = output {
|
if let Some(mut output) = output {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Copyright IBM Corp. 2024
|
// Copyright IBM Corp. 2024
|
||||||
|
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
|
mod additional;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
mod exchange;
|
mod exchange;
|
||||||
@@ -45,6 +46,7 @@ fn main() -> ExitCode {
|
|||||||
print_version!("2024", log_level; FEATURES.concat());
|
print_version!("2024", log_level; FEATURES.concat());
|
||||||
Ok(ExitCode::SUCCESS)
|
Ok(ExitCode::SUCCESS)
|
||||||
}
|
}
|
||||||
|
Command::Check(opt) => check(opt),
|
||||||
};
|
};
|
||||||
match res {
|
match res {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
|
|||||||
Reference in New Issue
Block a user