Skip to content
147 changes: 146 additions & 1 deletion examples/spdm_requester.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ use spdm_lib::commands::certificate::request::generate_get_certificate;
use spdm_lib::commands::challenge::{
request::generate_challenge_request, MeasurementSummaryHashType,
};
use spdm_lib::commands::measurements::request::{
generate_get_measurements, parse_measurements_response,
};
use spdm_lib::commands::measurements::MeasurementOperation;
use spdm_lib::context::SpdmContext;
use spdm_lib::error::SpdmError;
use spdm_lib::protocol::algorithms::{
Expand Down Expand Up @@ -469,7 +473,7 @@ fn full_flow(stream: TcpStream, config: &RequesterConfig) -> IoResult<()> {
println!("CHALLENGE_AUTH: {:x?}", &message_buffer.message_data());
}

if let Some(cert) = peer_leaf_cert {
if let Some(cert) = &peer_leaf_cert {
let pub_key = VerifyingKey::from_sec1_bytes(
cert.tbs_certificate()
.subject_public_key_info()
Expand All @@ -493,9 +497,100 @@ fn full_flow(stream: TcpStream, config: &RequesterConfig) -> IoResult<()> {
"CHALLENGE_AUTH signature verification failed",
));
}
spdm_context.set_authenticated();
println!("CHALLENGE_AUTH signature verification successfull");
}

// GET_MEASUREMENTS
message_buffer.reset();
generate_get_measurements(
&mut spdm_context,
&mut message_buffer,
false,
false,
MeasurementOperation::RequestAllMeasBlocks,
Some(0),
None,
)
.unwrap();
spdm_context
.requester_send_request(&mut message_buffer, EID)
.unwrap();

if config.verbose {
println!("GET_MEASUREMENTS: {:x?}", &message_buffer.message_data());
}

spdm_context
.requester_process_message(&mut message_buffer)
.unwrap();

if config.verbose {
println!("MEASUREMENTS: {:x?}", &message_buffer.message_data());
}

let measurements = parse_measurements_response(&message_buffer.message_data().unwrap())
Comment thread
embediver marked this conversation as resolved.
Outdated
.expect("Failed to parse measurement response");

if config.verbose {
println!(
"Measurements block count: {}",
measurements.total_measurement_blocks()
);
println!("Measurements Nonce: {}", HexString(&measurements.nonce));
Comment thread
embediver marked this conversation as resolved.
Outdated
if let Some(sig) = measurements.signature {
println!(
"Measurements Signature ({} bytes): {}",
sig.len(),
HexString(sig)
);
}
println!(
"Measurement content change status: {:?}",
measurements.content_changed()
);
}

if let Some(sig_raw) = measurements.signature {
if let Some(cert) = &peer_leaf_cert {
let pub_key = VerifyingKey::from_sec1_bytes(
cert.tbs_certificate()
.subject_public_key_info()
.subject_public_key
.as_bytes()
.unwrap(),
)
.unwrap();

let sig = Signature::from_slice(sig_raw).unwrap();
if !verify_measurements_signature(&mut spdm_context, pub_key, sig, config) {
eprintln!("MEASUREMENTS signature verification failed");
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"MEASUREMENTS signature verification failed",
));
}
println!("L1/L2 log verification successfull");
}
}

let mut meas_count = 0;
for measurement in measurements.iter() {
meas_count += 1;
if config.verbose {
println!(
"Parsed {:?} measurement with index {}",
measurement.measurement_spec, measurement.index
);
}
}

if meas_count != measurements.total_measurement_blocks() {
println!("[WARNING] measurement block count and parsed measurment count mismatch ({} expected, {} parsed)", measurements.total_measurement_blocks(), meas_count);
} else {
println!("Measurements retrieved successfully")
}

Ok(())
}

Expand Down Expand Up @@ -663,6 +758,56 @@ fn verify_challenge_auth_signature(
}
}

/// Currently only p384 support required
/// Here we verify that the responder and we created the same L1/L2 transcript and
/// that the signature is correct.
///
/// The transcript hash will be retrieved from the context.
/// The signature will be verified using the public key from the responder's certificate chain (which we already verified).
fn verify_measurements_signature(
ctx: &mut SpdmContext,
pubkey: VerifyingKey,
signature: Signature,
config: &RequesterConfig,
) -> bool {
use p384::ecdsa::signature::hazmat::PrehashVerifier;
use signature::Verifier;

let mut sig_combined_context = Vec::new();
if ctx.connection_info().version_number() >= SpdmVersion::V12 {
// since we verify the responder-generated signature, we have to use the same "responder-" context constant.
let sig_ctx = protocol::signature::create_responder_signing_context(
ctx.connection_info().version_number(),
protocol::ReqRespCode::Measurements,
)
.unwrap();
sig_combined_context.extend_from_slice(&sig_ctx);
if config.verbose {
println!(
"comb_ctx string: '{}'",
String::from_utf8_lossy(&sig_combined_context)
);
}
}

// Get the L1 transcript hash and verify the signature over it.
let mut transcript_hash = [0u8; 48];
ctx.transcript_hash(TranscriptContext::L1, &mut transcript_hash)
.unwrap();
if config.verbose {
println!("L1/2 hash: {}", HexString(&transcript_hash));
}

// M denotes the message that is signed. M shall be the concatenation of the combined_spdm_prefix and unverified_message_hash.
let m = [sig_combined_context.as_slice(), &transcript_hash].concat();

if ctx.connection_info().version_number() >= SpdmVersion::V12 {
pubkey.verify(&m, &signature).is_ok()
} else {
pubkey.verify_prehash(&m, &signature).is_ok()
}
}

#[derive(Debug)]
struct HexString<'a>(&'a [u8]);

Expand Down
2 changes: 1 addition & 1 deletion src/chunk_ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::commands::measurements_rsp::MeasurementsResponse;
use crate::commands::measurements::response::MeasurementsResponse;

#[derive(Debug, PartialEq)]
pub enum ChunkError {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/challenge/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ pub(crate) fn handle_challenge_auth_response<'a>(
// Append the entire message (excluding the signature) to the transcript before signature verification, as required by SPDM 1.2 and later.
ctx.append_message_to_transcript(resp_payload, TranscriptContext::M1)?;
resp_payload
.trim(tail - resp_payload.data_len())
.put_data(tail)
.map_err(|e| (true, CommandError::Codec(e)))?;

Ok(())
Expand Down
160 changes: 160 additions & 0 deletions src/commands/measurements/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright 2025
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! GET_MEASUREMENTS and MEASURMENTS command types and logic

/// Requester logic for GET_MEASUREMENTS and MEASURMENTS
pub mod request;
/// Responder logic for GET_MEASUREMENTS and MEASURMENTS
pub mod response;

use crate::protocol::*;
use crate::{codec::CommonCodec, error::CommandError};
use bitfield::bitfield;
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};

const RESPONSE_FIXED_FIELDS_SIZE: usize = 8;
const MAX_RESPONSE_VARIABLE_FIELDS_SIZE: usize =
NONCE_LEN + size_of::<u32>() + size_of::<RequesterContext>();

#[derive(FromBytes, IntoBytes, Immutable)]
#[repr(C)]
struct GetMeasurementsReqCommon {
req_attr: GetMeasurementsReqAttr,
meas_op: u8,
}
impl CommonCodec for GetMeasurementsReqCommon {}

#[derive(FromBytes, IntoBytes, Immutable)]
#[repr(C)]
struct GetMeasurementsReqSignature {
requester_nonce: [u8; NONCE_LEN],
slot_id: u8,
}
impl CommonCodec for GetMeasurementsReqSignature {}

bitfield! {
#[derive(FromBytes, IntoBytes, Immutable)]
#[repr(C)]
struct GetMeasurementsReqAttr(u8);
impl Debug;
u8;
pub signature_requested, set_signature_requested: 0, 0;
pub raw_bitstream_requested, set_raw_bitstream_requested: 1, 1;
pub new_measurement_requested, set_new_measurement_requested: 2, 2;
reserved, _: 7, 3;
}

bitfield! {
#[derive(FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
struct MeasurementsRspFixed([u8]);
impl Debug;
u8;
pub spdm_version, set_spdm_version: 7, 0;
pub req_resp_code, set_req_resp_code: 15, 8;
pub total_measurement_indices, set_total_measurement_indices: 23, 16;
pub slot_id, set_slot_id: 27, 24;
pub content_changed, set_content_changed: 29, 28;
reserved, _: 31, 30;
pub num_blocks, set_num_blocks: 39, 32;
pub measurement_record_len_byte0, set_measurement_record_len_byte0: 47, 40;
pub measurement_record_len_byte1, set_measurement_record_len_byte1: 55, 48;
pub measurement_record_len_byte2, set_measurement_record_len_byte2: 63, 56;
}

impl MeasurementsRspFixed<[u8; RESPONSE_FIXED_FIELDS_SIZE]> {
pub fn set_measurement_record_len(&mut self, len: u32) {
self.set_measurement_record_len_byte0((len & 0xFF) as u8);
self.set_measurement_record_len_byte1(((len >> 8) & 0xFF) as u8);
self.set_measurement_record_len_byte2(((len >> 16) & 0xFF) as u8);
}
}

impl Default for MeasurementsRspFixed<[u8; RESPONSE_FIXED_FIELDS_SIZE]> {
fn default() -> Self {
Self([0; RESPONSE_FIXED_FIELDS_SIZE])
}
}

impl CommonCodec for MeasurementsRspFixed<[u8; RESPONSE_FIXED_FIELDS_SIZE]> {}

/// Measurement Operation request field
pub enum MeasurementOperation {
/// Query the Responder for the total number of measurement blocks available
ReportMeasBlockCount,
/// Request the measurement block at a specific index
///
/// Index has to be between `0x01` and `0xFE`, inclusively.
RequestSingleMeasBlock(u8),
/// Request all measurement blocks
RequestAllMeasBlocks,
}

impl TryInto<u8> for MeasurementOperation {
type Error = CommandError;

fn try_into(self) -> Result<u8, Self::Error> {
match self {
MeasurementOperation::ReportMeasBlockCount => Ok(0x01),
MeasurementOperation::RequestSingleMeasBlock(x) => {
if matches!(x, 0x00 | 0xFF) {
Err(CommandError::UnsupportedRequest)
} else {
Ok(x)
}
}
MeasurementOperation::RequestAllMeasBlocks => Ok(0xFF),
}
}
}

#[derive(Debug, FromBytes, IntoBytes, Immutable, Unaligned, KnownLayout)]
#[repr(C, packed)]
struct MeasurementBlockHeader {
index: u8,
measurement_spec: MeasurementSpecification,
measurement_size: zerocopy::little_endian::U16,
}

impl CommonCodec for MeasurementBlockHeader {}

/// Content changed indicators for MEASUREMENT responses
#[derive(Debug, Clone, Copy)]
pub enum ContentChanged {
/// The Responder does not detect changes of MeasurementRecord fields
/// of previous MEASUREMENTS responses in the same measurement log,
/// or this message does not contain a signature.
NoDetection = 0x00,
/// The Responder detected that one or more MeasurementRecord fields
/// of previous MEASUREMENTS responses in the measurement log being
/// signed have changed. The Requester might consider issuing
/// GET_MEASUREMENTS again to acquire latest measurements.
ChangeDetected = 0x01,
/// The Responder detected no change in MeasurementRecord fields of
/// previous MEASUREMENTS responses in the measurement log being signed.
NoChangeDetected = 0x10,
Reserved = 0x11,
}

impl From<u8> for ContentChanged {
fn from(value: u8) -> Self {
match value {
0b00 => Self::NoDetection,
0b01 => Self::ChangeDetected,
0b10 => Self::NoChangeDetected,
_ => Self::Reserved,
}
}
}
Loading
Loading