From 8878ba4fd2823429d3b098d22b35c84bf62dea38 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Mon, 20 Apr 2026 11:21:26 +0200 Subject: [PATCH] pvattest: Add firmware check version 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Response version 2 which includes more details about the verification process. Reviewed-by: Marc Hartmayer Signed-off-by: Steffen Eiden Signed-off-by: Jan Höppner --- rust/pvattest/README.md | 10 ++ rust/pvattest/man/pvattest-check.1 | 16 ++- rust/pvattest/src/cli.rs | 21 ++++ rust/pvattest/src/cmd/check.rs | 9 +- rust/pvattest/src/cmd/check/firmware.rs | 138 ++++++++++++++++++++++++ 5 files changed, 190 insertions(+), 4 deletions(-) diff --git a/rust/pvattest/README.md b/rust/pvattest/README.md index d11c28d1..0ee88761 100644 --- a/rust/pvattest/README.md +++ b/rust/pvattest/README.md @@ -365,6 +365,16 @@ Check whether the firmware is supported by IBM. Requires internet access. +`--firmware-check-version ` +
    +Specify the firmware verification request version. + Default value: '1' + Possible values: + - **1**: Use firmware verification API request version 1.0. + - **2**: Use firmware verification API request version 2.0. +
+ + `--firmware-verify-url `
    Specify the endpoint to use for firmware version verification. Use an endpoint diff --git a/rust/pvattest/man/pvattest-check.1 b/rust/pvattest/man/pvattest-check.1 index 4984b221..294ee225 100644 --- a/rust/pvattest/man/pvattest-check.1 +++ b/rust/pvattest/man/pvattest-check.1 @@ -3,7 +3,7 @@ .\" it under the terms of the MIT license. See LICENSE for details. .\" -.TH "PVATTEST-CHECK" "1" "2026-05-19" "s390-tools" "Attestation Manual" +.TH "PVATTEST-CHECK" "1" "2026-06-23" "s390-tools" "Attestation Manual" .nh .ad l .SH NAME @@ -112,6 +112,20 @@ Required if add\-secret\-requests are specified. \-\-firmware .RS 4 Check whether the firmware is supported by IBM. Requires internet access. +.RE +.RE +.PP +\-\-firmware\-check\-version, \-\-fw\-ver +.RS 4 +Specify the firmware verification request version. +[default: '1'] + +Possible values: +.RS 4 +\- \fB1\fP: Use firmware verification API request version 1.0. + +\- \fB2\fP: Use firmware verification API request version 2.0. + .RE .RE .PP diff --git a/rust/pvattest/src/cli.rs b/rust/pvattest/src/cli.rs index 4f1a1fd1..c487dfc6 100644 --- a/rust/pvattest/src/cli.rs +++ b/rust/pvattest/src/cli.rs @@ -216,6 +216,17 @@ pub enum OutputType { Yaml, } +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Default)] +pub enum FirmwareCheckVersion { + /// Use firmware check API version 1.0. + #[default] + #[value(name = "1")] + V1, + /// Use firmware check API version 2.0. + #[value(name = "2")] + V2, +} + #[derive(Args, Debug)] pub struct CheckOpt { /// Specify the attestation response to check whether the policies are validated. @@ -303,6 +314,16 @@ pub struct CheckOpt { #[arg(long)] pub firmware: bool, + /// Specify the firmware verification request version. + #[arg( + long, + visible_alias("fw-ver"), + requires("firmware"), + value_enum, + default_value_t + )] + pub firmware_check_version: FirmwareCheckVersion, + /// Specify the endpoint to use for firmware version verification. /// /// Use an endpoint you trust. Requires the --firmware option. diff --git a/rust/pvattest/src/cmd/check.rs b/rust/pvattest/src/cmd/check.rs index 31957a28..9de8c105 100644 --- a/rust/pvattest/src/cmd/check.rs +++ b/rust/pvattest/src/cmd/check.rs @@ -15,11 +15,11 @@ use pv::misc::{create_file, open_file, read_file}; use serde::Serialize; use utils::HexSlice; -use self::firmware::firmware_check_v1; +use self::firmware::{firmware_check_v1, firmware_check_v2}; use self::host_key::{host_key_check, HostKeyCheck}; use self::secret_store::{secret_store_check, SecretStoreCheck}; use crate::additional::AttestationResult; -use crate::cli::{CheckOpt, CheckOptIO}; +use crate::cli::{CheckOpt, CheckOptIO, FirmwareCheckVersion}; use crate::exchange::ExchangeFormatResponse; #[derive(Default, Debug)] @@ -120,7 +120,10 @@ 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_v1(opt, &att_res)?; + let firmware_check = match opt.firmware_check_version { + FirmwareCheckVersion::V1 => firmware_check_v1(opt, &att_res)?, + FirmwareCheckVersion::V2 => firmware_check_v2(opt, &att_res)?, + }; let valid_firmware = match firmware_check { CheckState::None => None, CheckState::Data(_) => Some(true), diff --git a/rust/pvattest/src/cmd/check/firmware.rs b/rust/pvattest/src/cmd/check/firmware.rs index 4caceb12..f3e8ed9c 100644 --- a/rust/pvattest/src/cmd/check/firmware.rs +++ b/rust/pvattest/src/cmd/check/firmware.rs @@ -25,6 +25,8 @@ const CLIENT_ID: &str = "x-client-id: X"; enum Version { #[serde(rename = "1.0")] V1, + #[serde(rename = "2.0")] + V2, } trait Request: Serialize + Debug { @@ -50,6 +52,23 @@ impl Request for RequestV1_1 { } } +#[derive(Debug, Serialize, Deserialize)] +struct RequestV1_2 { + version: Version, + payload: String, +} + +impl Request for RequestV1_2 { + type Response = ResponseV2; + + fn new(firmware_hash: &[u8]) -> Self { + Self { + version: Version::V1, + payload: BASE64_STANDARD.encode(firmware_hash), + } + } +} + /// Trait for firmware verification response types. /// /// This trait defines the interface for handling responses from the IBM firmware @@ -75,6 +94,7 @@ trait Response: serde::de::DeserializeOwned + Debug + Display { fn verify_api(endp: &str) -> String { let ver = match Self::VERSION { Version::V1 => "v1", + Version::V2 => "v2", }; format!("{endp}/hmrs/firmware/attestation/{ver}/verify",) } @@ -113,6 +133,44 @@ impl Display for ResponseV1 { } } +// allow unused because all fields are provided by the REST API but may be unused by this toolk +#[allow(unused)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct VerifiedHashV2 { + hash: String, + signature: String, +} + +#[allow(unused)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ResponseV2 { + version: Version, + valid: bool, + reason: String, + verified_hashes: Vec, +} + +impl Response for ResponseV2 { + const VERSION: Version = Version::V2; + fn valid(&self) -> bool { + self.valid + } +} + +impl Display for ResponseV2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "The firmware is {}in a valid state", + if self.valid { "" } else { "not " } + )?; + let json = serde_json::to_string_pretty(self).map_err(|_| std::fmt::Error)?; + f.write_str(&json) + } +} + #[derive(Debug)] struct Buf(Vec); impl Handler for Buf { @@ -209,6 +267,10 @@ pub fn firmware_check_v1(opt: &CheckOpt, att_res: &AttestationResult) -> Result< firmware_check::(opt, att_res) } +pub fn firmware_check_v2(opt: &CheckOpt, att_res: &AttestationResult) -> Result> { + firmware_check::(opt, att_res) +} + #[cfg(test)] mod test { use super::*; @@ -226,6 +288,19 @@ mod test { assert_eq!(json, expected); } + #[test] + fn serialize_request_v1_2() { + let payload = BASE64_STANDARD.encode([42u8; 320]); + let req = RequestV1_2::new(&[42u8; 320]); + + assert!(matches!(req.version, Version::V1)); + assert_eq!(req.payload, payload); + + let json = serde_json::to_string(&req).unwrap(); + let expected = format!(r#"{{"version":"1.0","payload":"{payload}"}}"#); + assert_eq!(json, expected); + } + #[test] fn parse_response_v1() { let json = r#"{ @@ -264,4 +339,67 @@ mod test { let expected = "The firmware is in a valid state"; assert_eq!(display, expected); } + + #[test] + fn parse_response_v2() { + let verified_hashes: Vec<_> = (0u8..4) + .map(|i| { + ( + BASE64_STANDARD.encode([42u8 + i; 256]), + BASE64_STANDARD.encode([17u8 + i; 256]), + ) + }) + .collect(); + + let json = format!( + r#"{{ + "version": "2.0", + "valid": true, + "reason": "string", + "verifiedHashes": [ + {{ + "hash": "{}", + "signature": "{}" + }}, + {{ + "hash": "{}", + "signature": "{}" + }}, + {{ + "hash": "{}", + "signature": "{}" + }}, + {{ + "hash": "{}", + "signature": "{}" + }} + ] +}}"#, + verified_hashes[0].0, + verified_hashes[0].1, + verified_hashes[1].0, + verified_hashes[1].1, + verified_hashes[2].0, + verified_hashes[2].1, + verified_hashes[3].0, + verified_hashes[3].1, + ); + + let resp: ResponseV2 = serde_json::from_str(&json).unwrap(); + assert!(resp.valid()); + assert!(matches!(resp.version, Version::V2)); + assert_eq!(resp.reason, "string"); + assert_eq!(resp.verified_hashes.len(), 4); + + for (verified_hash, (hash, signature)) in + resp.verified_hashes.iter().zip(verified_hashes.iter()) + { + assert_eq!(&verified_hash.hash, hash); + assert_eq!(&verified_hash.signature, signature); + } + + let display = resp.to_string(); + let expected = format!("The firmware is in a valid state\n{json}"); + assert_eq!(display, expected); + } }