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