diff --git a/rust/pv/src/uvattest/additional.rs b/rust/pv/src/uvattest/additional.rs index 2370a35b..91dfc732 100644 --- a/rust/pv/src/uvattest/additional.rs +++ b/rust/pv/src/uvattest/additional.rs @@ -139,6 +139,19 @@ impl<'a, T: Serialize + From<&'a [u8]> + Sized> AdditionalData { 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 { + AdditionalData::<&'a [u8]>::from_slice(data, flags).map(Self::from_other) + } } impl<'a> AdditionalData<&'a [u8]> { diff --git a/rust/pvattest/src/additional.rs b/rust/pvattest/src/additional.rs new file mode 100644 index 00000000..a4c65f54 --- /dev/null +++ b/rust/pvattest/src/additional.rs @@ -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>, + #[serde(skip_serializing_if = "Option::is_none")] + pub add_fields: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_data: Option>, +} + +impl<'a> AttestationResult<'a> { + pub fn from_exchange( + resp: &'a ExchangeFormatResponse, + flags: &AttestationFlags, + ) -> Result { + 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(()) + } +} diff --git a/rust/pvattest/src/cli.rs b/rust/pvattest/src/cli.rs index 5ec9a6f5..96867156 100644 --- a/rust/pvattest/src/cli.rs +++ b/rust/pvattest/src/cli.rs @@ -2,6 +2,8 @@ // // Copyright IBM Corp. 2024 +use std::path::PathBuf; + use clap::{Args, Parser, Subcommand, ValueEnum, ValueHint}; use utils::{CertificateOptions, DeprecatedVerbosityOptions}; @@ -51,6 +53,11 @@ pub enum Command { /// Execution header of the image that was attested during ’pvattest perform’ 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. #[command(aliases(["--version"]), hide(true))] Version, @@ -191,7 +198,7 @@ pub struct VerifyOpt { /// 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 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,)] pub user_data: Option, } @@ -202,3 +209,73 @@ pub enum OutputType { #[default] 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, + + /// 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, + + /// 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, +} + +#[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() + } +} diff --git a/rust/pvattest/src/cmd.rs b/rust/pvattest/src/cmd.rs index a5cf7a32..ab5221fc 100644 --- a/rust/pvattest/src/cmd.rs +++ b/rust/pvattest/src/cmd.rs @@ -2,11 +2,13 @@ // // Copyright IBM Corp. 2024 // +pub mod check; pub mod create; #[cfg(target_arch = "s390x")] pub mod perform; pub mod verify; +pub use check::check; pub use create::create; pub use verify::verify; diff --git a/rust/pvattest/src/cmd/check.rs b/rust/pvattest/src/cmd/check.rs new file mode 100644 index 00000000..6f340af4 --- /dev/null +++ b/rust/pvattest/src/cmd/check.rs @@ -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 { + #[default] + None, + Data(T), + Err(String), +} + +impl CheckState { + fn check(self, issues: &mut Vec) -> Option { + match self { + Self::None => None, + Self::Data(d) => Some(d), + Self::Err(e) => { + issues.push(e.to_string()); + warn!("✘ {e}"); + None + } + } + } +} + +impl From> for CheckState { + fn from(value: Option) -> 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>> { + 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, + #[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>, +} + +/// Perform the policy checks +pub fn check(opt: &CheckOpt) -> Result { + 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)) + } + } +} diff --git a/rust/pvattest/src/cmd/check/host_key.rs b/rust/pvattest/src/cmd/check/host_key.rs new file mode 100644 index 00000000..bf8c4fe0 --- /dev/null +++ b/rust/pvattest/src/cmd/check/host_key.rs @@ -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>(hkds: &[A]) -> Result> { + 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> { + 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>> { + 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")) + )) + } +} diff --git a/rust/pvattest/src/cmd/verify.rs b/rust/pvattest/src/cmd/verify.rs index 50d8dff3..2a7469c6 100644 --- a/rust/pvattest/src/cmd/verify.rs +++ b/rust/pvattest/src/cmd/verify.rs @@ -5,71 +5,20 @@ use anyhow::Result; use log::{debug, warn}; use pv::{ - attest::{ - AdditionalData, AttestationFlags, AttestationItems, AttestationMeasurement, - AttestationRequest, - }, + attest::{AttestationItems, AttestationMeasurement, AttestationRequest}, misc::{create_file, open_file, read_exact_file, write_file}, request::{openssl::pkey::PKey, BootHdrTags, Confidential, SymKey}, }; -use serde::Serialize; -use std::{fmt::Display, process::ExitCode}; +use std::process::ExitCode; use utils::HexSlice; use crate::{ + additional::AttestationResult, cli::{OutputType, VerifyOpt}, exchange::ExchangeFormatResponse, EXIT_CODE_ATTESTATION_FAIL, }; -#[derive(Serialize)] -struct VerifyOutput<'a> { - cuid: HexSlice<'a>, - #[serde(skip_serializing_if = "Option::is_none")] - add: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - add_fields: Option>>, - #[serde(skip_serializing_if = "Option::is_none")] - user_data: Option>, -} - -impl<'a> VerifyOutput<'a> { - fn from_exchange(resp: &'a ExchangeFormatResponse, flags: &AttestationFlags) -> Result { - 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 { let mut input = open_file(&opt.input)?; let mut img = open_file(&opt.hdr)?; @@ -102,7 +51,7 @@ pub fn verify(opt: &VerifyOpt) -> Result { } warn!("Attestation measurement 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}"); if let Some(mut output) = output { diff --git a/rust/pvattest/src/main.rs b/rust/pvattest/src/main.rs index daf93614..6eb62124 100644 --- a/rust/pvattest/src/main.rs +++ b/rust/pvattest/src/main.rs @@ -3,6 +3,7 @@ // Copyright IBM Corp. 2024 #![allow(missing_docs)] +mod additional; mod cli; mod cmd; mod exchange; @@ -45,6 +46,7 @@ fn main() -> ExitCode { print_version!("2024", log_level; FEATURES.concat()); Ok(ExitCode::SUCCESS) } + Command::Check(opt) => check(opt), }; match res { Ok(c) => c,