pvattest: Add firmware check version 2

Add Response version 2 which includes more details about the
verification process.

Reviewed-by: Marc Hartmayer <marc@linux.ibm.com>
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
This commit is contained in:
Steffen Eiden
2026-04-20 11:21:26 +02:00
committed by Jan Höppner
parent e53f5ccfea
commit 8878ba4fd2
5 changed files with 190 additions and 4 deletions

View File

@@ -365,6 +365,16 @@ Check whether the firmware is supported by IBM. Requires internet access.
</ul>
`--firmware-check-version <FIRMWARE_CHECK_VERSION>`
<ul>
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.
</ul>
`--firmware-verify-url <URL>`
<ul>
Specify the endpoint to use for firmware version verification. Use an endpoint

View File

@@ -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 <FIRMWARE_CHECK_VERSION>
.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

View File

@@ -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.

View File

@@ -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<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_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),

View File

@@ -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<VerifiedHashV2>,
}
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<u8>);
impl Handler for Buf {
@@ -209,6 +267,10 @@ pub fn firmware_check_v1(opt: &CheckOpt, att_res: &AttestationResult) -> Result<
firmware_check::<RequestV1_1>(opt, att_res)
}
pub fn firmware_check_v2(opt: &CheckOpt, att_res: &AttestationResult) -> Result<CheckState<()>> {
firmware_check::<RequestV1_2>(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);
}
}