mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
rust/pvattest: Firmware version check
Check if the SE-guests machine firmware is in an IBM approved state. The machine firmware version can be obtained via setting a flag in the attestation request. The opaque 320 byte value from firmware is forwarded to an IBM server that verifies the firmware value and confirms if the machine is in an IBM approved firmware state. Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com> Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
This commit is contained in:
Generated
+9
@@ -72,6 +72,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -455,13 +461,16 @@ name = "pvattest"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"byteorder",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"curl",
|
||||
"log",
|
||||
"openssl",
|
||||
"s390_pv",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"utils",
|
||||
"zerocopy",
|
||||
|
||||
@@ -10,13 +10,16 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.70", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
byteorder = "1.3"
|
||||
clap = { version ="4.1", features = ["derive", "wrap_help"]}
|
||||
curl = "0.4.44"
|
||||
log = { version = "0.4.6", features = ["std", "release_max_level_debug"] }
|
||||
serde_yaml = "0.9"
|
||||
serde = { version = "1.0.139", features = ["derive"]}
|
||||
zerocopy = { version="0.7", features = ["derive"] }
|
||||
openssl = "0.10.57"
|
||||
serde = { version = "1.0.139", features = ["derive"]}
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
zerocopy = { version="0.7", features = ["derive"] }
|
||||
|
||||
pv = { path = "../pv", package = "s390_pv" }
|
||||
utils = { path = "../utils" }
|
||||
|
||||
@@ -102,6 +102,9 @@ pub enum AttAddFlags {
|
||||
|
||||
/// Request a hash over all successful Add-secret requests and the lock state as additional-data.
|
||||
SecretStoreHash,
|
||||
|
||||
/// Request the state of the firmware as additional-data.
|
||||
FirmwareState,
|
||||
}
|
||||
|
||||
// all members s390x only
|
||||
@@ -286,6 +289,18 @@ pub struct CheckOpt {
|
||||
/// Required if add-secret-requests are specified.
|
||||
#[arg(long, value_name = "BOOL")]
|
||||
pub secret_store_locked: Option<bool>,
|
||||
|
||||
/// Check whether the firmware is on an IBM supported version.
|
||||
///
|
||||
/// Requires internet access.
|
||||
#[arg(long)]
|
||||
pub firmware: bool,
|
||||
|
||||
/// Specify the endpoint to use for firmware version verification.
|
||||
///
|
||||
/// Use an endpoint you trust. Requires the --firmware option.
|
||||
#[arg(long, requires("firmware"), value_name = "URL", value_hint = ValueHint::Url)]
|
||||
pub firmware_verify_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
//
|
||||
// Copyright IBM Corp. 2024
|
||||
|
||||
mod firmware;
|
||||
mod host_key;
|
||||
mod secret_store;
|
||||
|
||||
use self::{
|
||||
firmware::firmware_check,
|
||||
host_key::{host_key_check, HostKeyCheck},
|
||||
secret_store::secret_store_check,
|
||||
secret_store::SecretStoreCheck,
|
||||
};
|
||||
use crate::{
|
||||
additional::AttestationResult, cli::CheckOpt, cmd::check::secret_store::secret_store_check,
|
||||
exchange::ExchangeFormatResponse,
|
||||
};
|
||||
use crate::{additional::AttestationResult, cli::CheckOpt, exchange::ExchangeFormatResponse};
|
||||
use anyhow::Result;
|
||||
use log::{debug, info, warn};
|
||||
use pv::{
|
||||
@@ -98,6 +98,8 @@ pub struct CheckResult<'a> {
|
||||
user_data: Option<HexSlice<'a>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
secret_store: Option<SecretStoreCheck<'a>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
valid_firmware: Option<bool>,
|
||||
}
|
||||
|
||||
/// Perform the policy checks
|
||||
@@ -118,6 +120,14 @@ pub fn check(opt: &CheckOpt) -> Result<ExitCode> {
|
||||
let user_data = user_data_check(opt, &att_res)?.check(&mut issues);
|
||||
let secret_store = secret_store_check(opt, &att_res)?.check(&mut issues);
|
||||
|
||||
let firmware_check = firmware_check(opt, &att_res)?;
|
||||
let valid_firmware = match firmware_check {
|
||||
CheckState::None => None,
|
||||
CheckState::Data(_) => Some(true),
|
||||
CheckState::Err(_) => Some(false),
|
||||
};
|
||||
firmware_check.check(&mut issues);
|
||||
|
||||
let res = CheckResult {
|
||||
successful: !issues.is_empty(),
|
||||
issues,
|
||||
@@ -125,6 +135,7 @@ pub fn check(opt: &CheckOpt) -> Result<ExitCode> {
|
||||
attest_host_key,
|
||||
user_data,
|
||||
secret_store,
|
||||
valid_firmware,
|
||||
};
|
||||
|
||||
debug!("res {res:?}");
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp. 2024
|
||||
|
||||
use std::{fmt::Display, time::Duration};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use base64::prelude::*;
|
||||
use curl::easy::{Easy2, Handler, List, WriteError};
|
||||
use log::{debug, info, log_enabled};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{bail_check, CheckState};
|
||||
use crate::{additional::AttestationResult, cli::CheckOpt};
|
||||
|
||||
const CHECK_DEFAULT_ENDP: &str = "https://www.ibm.com/support/resourcelink/api";
|
||||
const VERIFY_API: &str = "firmware-attestation/verify/v1";
|
||||
const TIMEOUT_MAX: Duration = Duration::from_secs(3);
|
||||
const USER_AGENT: &str = "s390-tools-pvattest";
|
||||
const CONTENT_TYPE: &str = "Content-Type: application/json";
|
||||
const CLIENT_ID: &str = "x-client-id: X";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Request {
|
||||
version: String,
|
||||
payload: String,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
const VERSION_ONE: &'static str = "1.0";
|
||||
|
||||
fn new_v1(firmware_hash: &[u8]) -> Self {
|
||||
Self {
|
||||
version: Self::VERSION_ONE.to_string(),
|
||||
payload: BASE64_STANDARD.encode(firmware_hash),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Response {
|
||||
version: String,
|
||||
valid: bool,
|
||||
reference_id: String,
|
||||
#[serde(default)]
|
||||
reason: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for Response {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"The firmware is {}in a valid state",
|
||||
if self.valid { "" } else { "not " }
|
||||
)?;
|
||||
|
||||
match &self.reason {
|
||||
Some(r) => write!(f, "\n Reason: {r}\n ReferenceId: {}", self.reference_id),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Buf(Vec<u8>);
|
||||
impl Handler for Buf {
|
||||
fn write(&mut self, data: &[u8]) -> std::result::Result<usize, WriteError> {
|
||||
self.0.extend_from_slice(data);
|
||||
Ok(data.len())
|
||||
}
|
||||
}
|
||||
|
||||
fn check<U: AsRef<[u8]>>(fw_hash: &U, endp: &str) -> Result<CheckState<()>> {
|
||||
let req = serde_json::to_vec(&Request::new_v1(fw_hash.as_ref()))?;
|
||||
|
||||
let url = format!("{endp}/{VERIFY_API}");
|
||||
debug!("POST {url}");
|
||||
|
||||
let mut http_header = List::new();
|
||||
http_header.append(CLIENT_ID)?;
|
||||
http_header.append(CONTENT_TYPE)?;
|
||||
|
||||
let mut handle = Easy2::new(Buf(Vec::with_capacity(0x1000)));
|
||||
handle.buffer_size(102400)?;
|
||||
handle.url(&url)?;
|
||||
handle.post_fields_copy(&req)?;
|
||||
handle.http_headers(http_header)?;
|
||||
handle.useragent(USER_AGENT)?;
|
||||
handle.max_redirections(50)?;
|
||||
handle.post(true)?;
|
||||
handle.timeout(TIMEOUT_MAX)?;
|
||||
handle.follow_location(true)?;
|
||||
if log_enabled!(log::Level::Trace) {
|
||||
handle.verbose(true)?;
|
||||
}
|
||||
handle.perform()?;
|
||||
|
||||
if handle.response_code()? != 200 {
|
||||
bail!(
|
||||
"The firmware verification server responded with http response status code '{}'",
|
||||
handle.response_code()?
|
||||
);
|
||||
}
|
||||
|
||||
let resp: Response = match serde_json::from_slice(&handle.get_ref().0) {
|
||||
Ok(res) => res,
|
||||
Err(e) => bail!(
|
||||
"Unexpected response from server: {} \n (\"{}\")",
|
||||
String::from_utf8(handle.get_ref().0.clone())
|
||||
.unwrap_or_else(|_| "No UTF-8 message".to_string()),
|
||||
e
|
||||
),
|
||||
};
|
||||
|
||||
debug!("Firmware check {resp:?}");
|
||||
|
||||
match resp.valid {
|
||||
true => info!("✓ {resp}"),
|
||||
false => bail_check!(&format!("{resp}")),
|
||||
}
|
||||
|
||||
Ok(CheckState::Data(()))
|
||||
}
|
||||
|
||||
pub fn firmware_check(opt: &CheckOpt, att_res: &AttestationResult) -> Result<CheckState<()>> {
|
||||
if !opt.firmware {
|
||||
return Ok(None.into());
|
||||
}
|
||||
|
||||
let endp = opt
|
||||
.firmware_verify_url
|
||||
.as_deref()
|
||||
.unwrap_or(CHECK_DEFAULT_ENDP);
|
||||
|
||||
match att_res
|
||||
.add_fields
|
||||
.as_ref()
|
||||
.and_then(|add| add.firmware_state())
|
||||
{
|
||||
Some(hash) => check(hash, endp),
|
||||
None => {
|
||||
bail_check!(
|
||||
"The Attestation response contains no firmware hash, but checking was enabled"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ fn flags(cli_flags: &[AttAddFlags]) -> AttestationFlags {
|
||||
AttAddFlags::PhkhImg => att_flags.set_image_phkh(),
|
||||
AttAddFlags::PhkhAtt => att_flags.set_attest_phkh(),
|
||||
AttAddFlags::SecretStoreHash => att_flags.set_secret_store_hash(),
|
||||
AttAddFlags::FirmwareState => att_flags.set_firmware_state(),
|
||||
}
|
||||
}
|
||||
att_flags
|
||||
|
||||
Reference in New Issue
Block a user