From 7cc131880c4db9e3bac2758b1e3ce8e089c0250d Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Mon, 18 Nov 2024 14:50:21 +0100 Subject: [PATCH] rust/pvattest: Firmware version check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Steffen Eiden --- rust/Cargo.lock | 9 ++ rust/pvattest/Cargo.toml | 9 +- rust/pvattest/src/cli.rs | 15 +++ rust/pvattest/src/cmd/check.rs | 19 ++- rust/pvattest/src/cmd/check/firmware.rs | 149 ++++++++++++++++++++++++ rust/pvattest/src/cmd/create.rs | 1 + 6 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 rust/pvattest/src/cmd/check/firmware.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 6fbd8ade..16752ec9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -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", diff --git a/rust/pvattest/Cargo.toml b/rust/pvattest/Cargo.toml index 22ec5325..d036c0e7 100644 --- a/rust/pvattest/Cargo.toml +++ b/rust/pvattest/Cargo.toml @@ -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" } diff --git a/rust/pvattest/src/cli.rs b/rust/pvattest/src/cli.rs index f44b127d..a91759a7 100644 --- a/rust/pvattest/src/cli.rs +++ b/rust/pvattest/src/cli.rs @@ -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, + + /// 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, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] diff --git a/rust/pvattest/src/cmd/check.rs b/rust/pvattest/src/cmd/check.rs index eb6b0663..d2b1a6cc 100644 --- a/rust/pvattest/src/cmd/check.rs +++ b/rust/pvattest/src/cmd/check.rs @@ -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>, #[serde(skip_serializing_if = "Option::is_none")] secret_store: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + valid_firmware: Option, } /// Perform the policy checks @@ -118,6 +120,14 @@ pub fn check(opt: &CheckOpt) -> Result { 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 { attest_host_key, user_data, secret_store, + valid_firmware, }; debug!("res {res:?}"); diff --git a/rust/pvattest/src/cmd/check/firmware.rs b/rust/pvattest/src/cmd/check/firmware.rs new file mode 100644 index 00000000..81bceee2 --- /dev/null +++ b/rust/pvattest/src/cmd/check/firmware.rs @@ -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, +} + +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); +impl Handler for Buf { + fn write(&mut self, data: &[u8]) -> std::result::Result { + self.0.extend_from_slice(data); + Ok(data.len()) + } +} + +fn check>(fw_hash: &U, endp: &str) -> Result> { + 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> { + 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" + ) + } + } +} diff --git a/rust/pvattest/src/cmd/create.rs b/rust/pvattest/src/cmd/create.rs index 4e797530..40c17775 100644 --- a/rust/pvattest/src/cmd/create.rs +++ b/rust/pvattest/src/cmd/create.rs @@ -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