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:
Steffen Eiden
2024-11-18 14:50:15 +01:00
parent ac7f866237
commit 697dcc0f6b
8 changed files with 467 additions and 56 deletions
+133
View 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
View 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"))
))
}
}
+4 -55
View File
@@ -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<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> {
let mut input = open_file(&opt.input)?;
let mut img = open_file(&opt.hdr)?;
@@ -102,7 +51,7 @@ pub fn verify(opt: &VerifyOpt) -> Result<ExitCode> {
}
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 {