Add host-side tests for SPDM This adds host-side tests for the SPDM stack through VCA, which does not require cryptographic services. It utilzes a similar flow to the MCTP echo test, using the MCTP stack objects to utilize the DirectClient loopback functionality. Some of the mock functionality may be fleshed out in the future, but we already know the full requester and responder stacks function. * Resolves an undersized buffer issue. ** Eliminated magic numbers in tests. * Clears sent_tag after response is received
diff --git a/services/mctp/api/src/stack.rs b/services/mctp/api/src/stack.rs index fead571..a54103a 100644 --- a/services/mctp/api/src/stack.rs +++ b/services/mctp/api/src/stack.rs
@@ -128,6 +128,8 @@ } let meta = self.stack.client.recv(self.handle, self.timeout, buf)?; let len = meta.payload_size; + // Clear sent_tag after receiving response, allowing another send/recv cycle + self.sent_tag = None; Ok((meta, &mut buf[..len])) }
diff --git a/services/mctp/echo/tests/echo_host.rs b/services/mctp/echo/tests/echo_host.rs index 33e1288..68c5fcd 100644 --- a/services/mctp/echo/tests/echo_host.rs +++ b/services/mctp/echo/tests/echo_host.rs
@@ -16,6 +16,11 @@ use openprot_mctp_echo::{echo_once, prepare_listener, ECHO_MSG_TYPE}; use openprot_mctp_server::Server; +/// MTU for MCTP payload (without header) +const MCTP_MTU: usize = 255; +/// MCTP header size (4 bytes) +const MCTP_HEADER_SIZE: usize = 4; + struct BufferSender<'a> { packets: &'a RefCell<Vec<Vec<u8>>>, } @@ -27,7 +32,8 @@ payload: &[&[u8]], ) -> mctp::Result<Tag> { loop { - let mut buf = [0u8; 255]; + // Buffer must be MTU + header size + let mut buf = [0u8; MCTP_MTU + MCTP_HEADER_SIZE]; match fragmenter.fragment_vectored(payload, &mut buf) { SendOutput::Packet(p) => self.packets.borrow_mut().push(p.to_vec()), SendOutput::Complete { tag, .. } => return Ok(tag), @@ -37,7 +43,7 @@ } fn get_mtu(&self) -> usize { - 255 + MCTP_MTU } }
diff --git a/services/mctp/server/tests/common/mod.rs b/services/mctp/server/tests/common/mod.rs index 992eb1a..b76befb 100644 --- a/services/mctp/server/tests/common/mod.rs +++ b/services/mctp/server/tests/common/mod.rs
@@ -38,6 +38,11 @@ pub packets: &'a RefCell<Vec<Vec<u8>>>, } +/// MTU for MCTP payload (without header) +const MCTP_MTU: usize = 255; +/// MCTP header size (4 bytes) +const MCTP_HEADER_SIZE: usize = 4; + impl Sender for BufferSender<'_> { fn send_vectored( &mut self, @@ -45,7 +50,8 @@ payload: &[&[u8]], ) -> mctp::Result<Tag> { loop { - let mut buf = [0u8; 255]; + // Buffer must be MTU + header size + let mut buf = [0u8; MCTP_MTU + MCTP_HEADER_SIZE]; match fragmenter.fragment_vectored(payload, &mut buf) { SendOutput::Packet(p) => { self.packets.borrow_mut().push(p.to_vec()); @@ -57,7 +63,7 @@ } fn get_mtu(&self) -> usize { - 255 + MCTP_MTU } } @@ -78,7 +84,8 @@ payload: &[&[u8]], ) -> mctp::Result<Tag> { loop { - let mut buf = [0u8; 255]; + // Buffer must be MTU + header size + let mut buf = [0u8; MCTP_MTU + MCTP_HEADER_SIZE]; match fragmenter.fragment_vectored(payload, &mut buf) { SendOutput::Packet(p) => { self.packets.borrow_mut().push(p.to_vec()); @@ -111,7 +118,8 @@ payload: &[&[u8]], ) -> mctp::Result<Tag> { loop { - let mut buf = [0u8; 255]; + // Buffer must be MTU + header size + let mut buf = [0u8; MCTP_MTU + MCTP_HEADER_SIZE]; match fragmenter.fragment_vectored(payload, &mut buf) { SendOutput::Packet(_) => {} SendOutput::Complete { tag, .. } => return Ok(tag), @@ -121,7 +129,7 @@ } fn get_mtu(&self) -> usize { - 255 + MCTP_MTU } }
diff --git a/services/mctp/server/tests/integration.rs b/services/mctp/server/tests/integration.rs index 61ab5eb..c929b64 100644 --- a/services/mctp/server/tests/integration.rs +++ b/services/mctp/server/tests/integration.rs
@@ -547,3 +547,4 @@ stack.set_eid(99).expect("set_eid should succeed"); assert_eq!(stack.get_eid(), 99); } +
diff --git a/services/spdm/requester/BUILD.bazel b/services/spdm/requester/BUILD.bazel index 0362cd5..6ed620d 100644 --- a/services/spdm/requester/BUILD.bazel +++ b/services/spdm/requester/BUILD.bazel
@@ -19,3 +19,24 @@ name = "spdm_requester_test", crate = ":spdm_requester_lib", ) + +# Host integration test: SPDM VCA flow with in-memory MCTP transport. +# Uses the same pattern as //services/mctp/server:mctp_server_echo_test. +rust_test( + name = "spdm_requester_vca_host_test", + srcs = [ + "tests/common/mod.rs", + "tests/vca_host.rs", + ], + crate_root = "tests/vca_host.rs", + edition = "2024", + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/spdm/transport-mctp:spdm_transport_mctp", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + "@rust_crates//:spdm-lib", + "@rust_crates//:zerocopy", + ], +)
diff --git a/services/spdm/requester/tests/common/mod.rs b/services/spdm/requester/tests/common/mod.rs new file mode 100644 index 0000000..bc31c3c --- /dev/null +++ b/services/spdm/requester/tests/common/mod.rs
@@ -0,0 +1,688 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Shared test fixtures for SPDM host integration tests. +//! +//! Provides: +//! - [`BufferSender`] — captures outbound packets into a `Vec` +//! - [`DirectClient`] — implements `MctpClient` directly against a `Server` +//! - [`MockCertStore`] — mock certificate store for SPDM +//! - [`MockHash`] — mock hash implementation +//! - [`MockRng`] — mock RNG implementation +//! - [`MockEvidence`] — mock evidence/measurements provider +//! - [`DemoPeerCertStore`] — peer certificate store for requester + +#![allow(dead_code)] + +use std::cell::RefCell; + +use mctp::{Eid, Tag}; +use mctp_lib::fragment::{Fragmenter, SendOutput}; +use mctp_lib::Sender; +use openprot_mctp_api::{ + Handle, MctpClient, MctpError, RecvMetadata, ResponseCode, +}; +use openprot_mctp_server::Server; + +use spdm_lib::cert_store::{CertStoreError, CertStoreResult, PeerCertStore, SpdmCertStore}; +use spdm_lib::commands::challenge::MeasurementSummaryHashType; +use spdm_lib::platform::evidence::{SpdmEvidence, SpdmEvidenceError, SpdmEvidenceResult}; +use spdm_lib::platform::hash::{SpdmHash, SpdmHashAlgoType, SpdmHashError, SpdmHashResult}; +use spdm_lib::platform::rng::{SpdmRng, SpdmRngResult}; +use spdm_lib::protocol::algorithms::{AsymAlgo, ECC_P384_SIGNATURE_SIZE, SHA384_HASH_SIZE}; +use spdm_lib::protocol::certs::{CertificateInfo, KeyUsageMask}; +use spdm_lib::protocol::{BaseHashAlgoType, SpdmCertChainHeader}; +use zerocopy::FromBytes; + +// --------------------------------------------------------------------------- +// BufferSender — captures outbound MCTP packets +// --------------------------------------------------------------------------- + +/// A mock [`Sender`] that captures every outbound MCTP packet into a shared buffer. +pub struct BufferSender<'a> { + pub packets: &'a RefCell<Vec<Vec<u8>>>, +} + +/// MTU for MCTP payload (without header) +const MCTP_MTU: usize = 255; +/// MCTP header size (4 bytes) +const MCTP_HEADER_SIZE: usize = 4; + +impl Sender for BufferSender<'_> { + fn send_vectored( + &mut self, + mut fragmenter: Fragmenter, + payload: &[&[u8]], + ) -> mctp::Result<Tag> { + loop { + // Buffer must be MTU + header size + let mut buf = [0u8; MCTP_MTU + MCTP_HEADER_SIZE]; + match fragmenter.fragment_vectored(payload, &mut buf) { + SendOutput::Packet(p) => { + self.packets.borrow_mut().push(p.to_vec()); + } + SendOutput::Complete { tag, .. } => return Ok(tag), + SendOutput::Error { err, .. } => return Err(err), + } + } + } + + fn get_mtu(&self) -> usize { + MCTP_MTU + } +} + +// --------------------------------------------------------------------------- +// transfer — moves packets between servers +// --------------------------------------------------------------------------- + +/// Drain `packets` into `dest` as inbound MCTP packets. +pub fn transfer<S: Sender, const N: usize>( + packets: &RefCell<Vec<Vec<u8>>>, + dest: &mut Server<S, N>, +) { + let pkts = packets.borrow(); + for pkt in pkts.iter() { + dest.inbound(pkt).unwrap(); + } +} + +// --------------------------------------------------------------------------- +// DirectClient — implements MctpClient against a Server +// --------------------------------------------------------------------------- + +/// Implements [`MctpClient`] by calling [`Server`] methods directly. +pub struct DirectClient<'a, S: Sender, const N: usize> { + pub server: &'a RefCell<Server<S, N>>, +} + +impl<'a, S: Sender, const N: usize> DirectClient<'a, S, N> { + pub fn new(server: &'a RefCell<Server<S, N>>) -> Self { + Self { server } + } +} + +impl<S: Sender, const N: usize> MctpClient for DirectClient<'_, S, N> { + fn req(&self, eid: u8) -> Result<Handle, MctpError> { + self.server.borrow_mut().req(eid) + } + + fn listener(&self, msg_type: u8) -> Result<Handle, MctpError> { + self.server.borrow_mut().listener(msg_type) + } + + fn get_eid(&self) -> u8 { + self.server.borrow().get_eid() + } + + fn set_eid(&self, eid: u8) -> Result<(), MctpError> { + self.server.borrow_mut().set_eid(eid) + } + + fn recv( + &self, + handle: Handle, + _timeout_millis: u32, + buf: &mut [u8], + ) -> Result<RecvMetadata, MctpError> { + self.server + .borrow_mut() + .try_recv(handle, buf) + .ok_or(MctpError::from_code(ResponseCode::TimedOut)) + } + + fn send( + &self, + handle: Option<Handle>, + msg_type: u8, + eid: Option<u8>, + tag: Option<u8>, + integrity_check: bool, + buf: &[u8], + ) -> Result<u8, MctpError> { + self.server + .borrow_mut() + .send(handle, msg_type, eid, tag, integrity_check, buf) + } + + fn drop_handle(&self, handle: Handle) { + let _ = self.server.borrow_mut().unbind(handle); + } +} + +// --------------------------------------------------------------------------- +// make_server helper +// --------------------------------------------------------------------------- + +/// Construct a `Server` + its outbound packet buffer. +pub fn make_server(eid: u8, packets: &RefCell<Vec<Vec<u8>>>) -> Server<BufferSender<'_>, 16> { + Server::new(Eid(eid), 0, BufferSender { packets }) +} + +// --------------------------------------------------------------------------- +// MockCertStore — mock certificate store +// --------------------------------------------------------------------------- + +/// Mock certificate store with fixed placeholder data. +pub struct MockCertStore; + +impl MockCertStore { + pub fn new() -> Self { + Self + } +} + +impl SpdmCertStore for MockCertStore { + fn slot_count(&self) -> u8 { + 1 + } + + fn is_provisioned(&self, slot_id: u8) -> bool { + slot_id == 0 + } + + fn cert_chain_len(&mut self, asym_algo: AsymAlgo, slot_id: u8) -> CertStoreResult<usize> { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + Ok(32) + } + + fn get_cert_chain( + &mut self, + slot_id: u8, + asym_algo: AsymAlgo, + offset: usize, + cert_portion: &mut [u8], + ) -> CertStoreResult<usize> { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + + const CERT_CHAIN: [u8; 32] = [0xAA; 32]; + + if offset >= CERT_CHAIN.len() { + return Err(CertStoreError::InvalidOffset); + } + + let remaining = CERT_CHAIN.len() - offset; + let to_copy = remaining.min(cert_portion.len()); + cert_portion[..to_copy].copy_from_slice(&CERT_CHAIN[offset..offset + to_copy]); + + if to_copy < cert_portion.len() { + cert_portion[to_copy..].fill(0); + } + + Ok(to_copy) + } + + fn root_cert_hash( + &mut self, + slot_id: u8, + asym_algo: AsymAlgo, + cert_hash: &mut [u8; SHA384_HASH_SIZE], + ) -> CertStoreResult<()> { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + + const ROOT_HASH: [u8; SHA384_HASH_SIZE] = [0xBB; SHA384_HASH_SIZE]; + cert_hash.copy_from_slice(&ROOT_HASH); + Ok(()) + } + + fn sign_hash( + &self, + slot_id: u8, + _hash: &[u8; SHA384_HASH_SIZE], + signature: &mut [u8; ECC_P384_SIGNATURE_SIZE], + ) -> CertStoreResult<()> { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + + const SIGNATURE: [u8; ECC_P384_SIGNATURE_SIZE] = [0xCC; ECC_P384_SIGNATURE_SIZE]; + signature.copy_from_slice(&SIGNATURE); + Ok(()) + } + + fn key_pair_id(&self, _slot_id: u8) -> Option<u8> { + None + } + + fn cert_info(&self, _slot_id: u8) -> Option<CertificateInfo> { + None + } + + fn key_usage_mask(&self, _slot_id: u8) -> Option<KeyUsageMask> { + None + } +} + +// --------------------------------------------------------------------------- +// MockHash — mock hash implementation +// --------------------------------------------------------------------------- + +/// Mock hash implementation that XORs all input bytes. +pub struct MockHash { + buffer: Vec<u8>, + algo: Option<SpdmHashAlgoType>, +} + +impl MockHash { + pub fn new() -> Self { + Self { + buffer: Vec::new(), + algo: None, + } + } +} + +impl SpdmHash for MockHash { + fn init(&mut self, algo: SpdmHashAlgoType, _secret: Option<&[u8]>) -> SpdmHashResult<()> { + self.buffer.clear(); + self.algo = Some(algo); + Ok(()) + } + + fn update(&mut self, data: &[u8]) -> SpdmHashResult<()> { + self.buffer.extend_from_slice(data); + Ok(()) + } + + fn finalize(&mut self, dest: &mut [u8]) -> SpdmHashResult<()> { + let hash_size = match self.algo { + Some(SpdmHashAlgoType::SHA384) => 48, + Some(SpdmHashAlgoType::SHA512) => 64, + _ => return Err(SpdmHashError::InvalidAlgorithm), + }; + + if dest.len() < hash_size { + return Err(SpdmHashError::BufferTooSmall); + } + + let mut hash_byte = 0u8; + for byte in &self.buffer { + hash_byte ^= byte; + } + dest[..hash_size].fill(hash_byte); + + Ok(()) + } + + fn hash(&mut self, algo: SpdmHashAlgoType, data: &[u8], dest: &mut [u8]) -> SpdmHashResult<()> { + self.init(algo, None)?; + self.update(data)?; + self.finalize(dest) + } + + fn reset(&mut self) { + self.buffer.clear(); + self.algo = None; + } + + fn algo(&self) -> SpdmHashAlgoType { + self.algo.unwrap_or(SpdmHashAlgoType::SHA384) + } +} + +// --------------------------------------------------------------------------- +// MockRng — mock RNG implementation +// --------------------------------------------------------------------------- + +/// Mock RNG that produces deterministic incrementing patterns. +pub struct MockRng; + +impl MockRng { + pub fn new() -> Self { + Self + } +} + +impl SpdmRng for MockRng { + fn get_random_bytes(&mut self, buf: &mut [u8]) -> SpdmRngResult<()> { + for (i, byte) in buf.iter_mut().enumerate() { + *byte = (i & 0xFF) as u8; + } + Ok(()) + } + + fn generate_random_number(&mut self, random_number: &mut [u8]) -> SpdmRngResult<()> { + for (i, byte) in random_number.iter_mut().enumerate() { + *byte = ((i + 0x42) & 0xFF) as u8; + } + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// MockEvidence — mock evidence/measurements provider +// --------------------------------------------------------------------------- + +/// Mock evidence implementation with fixed measurements. +pub struct MockEvidence; + +impl MockEvidence { + pub fn new() -> Self { + Self + } +} + +impl SpdmEvidence for MockEvidence { + fn pcr_quote_size(&self, _with_pqc_sig: bool) -> SpdmEvidenceResult<usize> { + // Format: count(1) + [index(1) + size(2) + data]*2 + Ok(1 + (1 + 2 + 23) + (1 + 2 + 20)) + } + + fn pcr_quote(&self, dest: &mut [u8], _with_pqc_sig: bool) -> SpdmEvidenceResult<usize> { + let required_size = self.pcr_quote_size(false)?; + if dest.len() < required_size { + return Err(SpdmEvidenceError::InvalidEvidenceFormat); + } + + let mut offset = 0; + + // Measurement count + dest[offset] = 2; + offset += 1; + + // Measurement 0 + dest[offset] = 0; + offset += 1; + dest[offset..offset + 2].copy_from_slice(&23u16.to_le_bytes()); + offset += 2; + dest[offset..offset + 23].copy_from_slice(b"OpenPRoT SPDM Loopback"); + offset += 23; + + // Measurement 1 + dest[offset] = 1; + offset += 1; + dest[offset..offset + 2].copy_from_slice(&20u16.to_le_bytes()); + offset += 2; + dest[offset..offset + 20].copy_from_slice(b"MCTP Loopback Test "); + offset += 20; + + Ok(offset) + } +} + +// --------------------------------------------------------------------------- +// PeerSlot — peer certificate slot data +// --------------------------------------------------------------------------- + +#[derive(Debug, Default)] +pub struct PeerSlot { + pub cert_chain: Vec<u8>, + pub digest: Vec<u8>, + pub keypair_id: Option<u8>, + pub certificate_info: Option<CertificateInfo>, + pub key_usage_mask: Option<KeyUsageMask>, + pub requested_msh_type: Option<MeasurementSummaryHashType>, +} + +impl PeerSlot { + fn get_root_hash(&self, hash_algo: BaseHashAlgoType) -> Option<&[u8]> { + let (length, rest) = SpdmCertChainHeader::ref_from_prefix(&self.cert_chain).ok()?; + if length.get_length() != self.cert_chain.len() as u32 { + return None; + } + Some(&rest[..hash_algo.hash_byte_size()]) + } + + fn get_cert_chain(&self, hash_algo: BaseHashAlgoType) -> Option<&[u8]> { + let (length, rest) = SpdmCertChainHeader::ref_from_prefix(&self.cert_chain).ok()?; + if length.get_length() != self.cert_chain.len() as u32 { + return None; + } + Some(&rest[hash_algo.hash_byte_size()..]) + } +} + +// --------------------------------------------------------------------------- +// DemoPeerCertStore — peer certificate store for requester +// --------------------------------------------------------------------------- + +/// Peer certificate store for SPDM requester (stores responder's certificates). +#[derive(Debug, Default)] +pub struct DemoPeerCertStore { + supported_slots_mask: u8, + provisioned_slots_mask: u8, + pub peer_slots: Vec<Option<PeerSlot>>, +} + +impl DemoPeerCertStore { + pub fn new() -> Self { + let mut slots = Vec::new(); + slots.push(None); + Self { + supported_slots_mask: 0, + provisioned_slots_mask: 0, + peer_slots: slots, + } + } +} + +impl PeerCertStore for DemoPeerCertStore { + fn slot_count(&self) -> u8 { + self.peer_slots.len() as u8 + } + + fn assemble( + &mut self, + slot_id: u8, + portion: &[u8], + ) -> Result<spdm_lib::cert_store::ReassemblyStatus, CertStoreError> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + + slot.cert_chain.extend_from_slice(portion); + Ok(spdm_lib::cert_store::ReassemblyStatus::InProgress) + } + + fn reset(&mut self, slot_id: u8) { + if let Some(Some(slot)) = self.peer_slots.get_mut(slot_id as usize) { + *slot = PeerSlot::default(); + } + } + + fn get_raw_chain(&self, slot_id: u8) -> CertStoreResult<&[u8]> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + Ok(&slot.cert_chain) + } + + fn get_cert_chain(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + slot.get_cert_chain(hash_algo) + .ok_or(CertStoreError::CertReadError) + } + + fn set_supported_slots(&mut self, slot_mask: u8) -> CertStoreResult<()> { + for b in 0..8 { + if slot_mask & (1 << b) != 0 { + if let Some(slot) = self.peer_slots.get_mut(b as usize) { + if slot.is_none() { + *slot = Some(PeerSlot::default()); + } + } + } + } + self.supported_slots_mask = slot_mask; + Ok(()) + } + + fn get_supported_slots(&self) -> CertStoreResult<u8> { + Ok(self.supported_slots_mask) + } + + fn set_provisioned_slots(&mut self, provisioned_slot_mask: u8) -> CertStoreResult<()> { + self.provisioned_slots_mask = provisioned_slot_mask; + Ok(()) + } + + fn get_provisioned_slots(&self) -> CertStoreResult<u8> { + Ok(self.provisioned_slots_mask) + } + + fn set_cert_chain(&mut self, slot_id: u8, cert_chain: &[u8]) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + + slot.cert_chain = cert_chain.to_vec(); + Ok(()) + } + + fn get_digest(&self, slot_id: u8) -> CertStoreResult<&[u8]> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + Ok(&slot.digest) + } + + fn set_digest(&mut self, slot_id: u8, digest: &[u8]) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + slot.digest = digest.to_vec(); + Ok(()) + } + + fn get_cert_info(&self, slot_id: u8) -> CertStoreResult<CertificateInfo> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + slot.certificate_info + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_cert_info(&mut self, slot_id: u8, cert_info: CertificateInfo) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + slot.certificate_info = Some(cert_info); + Ok(()) + } + + fn get_key_usage_mask(&self, slot_id: u8) -> CertStoreResult<KeyUsageMask> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + slot.key_usage_mask + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_key_usage_mask( + &mut self, + slot_id: u8, + key_usage_mask: KeyUsageMask, + ) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + slot.key_usage_mask = Some(key_usage_mask); + Ok(()) + } + + fn get_keypair(&self, slot_id: u8) -> CertStoreResult<u8> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + slot.keypair_id + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_keypair(&mut self, slot_id: u8, keypair: u8) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + slot.keypair_id = Some(keypair); + Ok(()) + } + + fn get_root_hash(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + slot.get_root_hash(hash_algo) + .ok_or(CertStoreError::CertReadError) + } + + fn get_requested_msh_type(&self, slot_id: u8) -> CertStoreResult<MeasurementSummaryHashType> { + self.peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)? + .requested_msh_type + .clone() + .ok_or(CertStoreError::Undefined) + } + + fn set_requested_msh_type( + &mut self, + slot_id: u8, + msh_type: MeasurementSummaryHashType, + ) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + slot.requested_msh_type = Some(msh_type); + Ok(()) + } +}
diff --git a/services/spdm/requester/tests/vca_host.rs b/services/spdm/requester/tests/vca_host.rs new file mode 100644 index 0000000..2659060 --- /dev/null +++ b/services/spdm/requester/tests/vca_host.rs
@@ -0,0 +1,358 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! SPDM VCA (Version, Capabilities, Algorithms) host integration test. +//! +//! Exercises the full SPDM requester ↔ responder flow using the same +//! in-memory transport pattern as the MCTP echo tests. No IPC, no QEMU — +//! runs entirely on the host. +//! +//! ## Architecture +//! +//! ```text +//! ┌─ Requester (EID 8) ────────┐ ┌─ Responder (EID 42) ───────┐ +//! │ Stack<DirectClient> │ │ Stack<DirectClient> │ +//! │ └→ MctpSpdmTransport │ │ └→ MctpSpdmTransport │ +//! │ └→ SpdmContext │ │ └→ SpdmContext │ +//! └────────────────────────────┘ └────────────────────────────┘ +//! │ │ +//! └──── BufferSender ←─ transfer() ───┘ +//! ``` + +mod common; + +use std::cell::RefCell; + +use mctp::Eid; +use openprot_mctp_api::stack::Stack; +#[allow(unused_imports)] +use openprot_mctp_api::MctpClient; +use openprot_mctp_server::Server; +use openprot_spdm_transport_mctp::MctpSpdmTransport; +use spdm_lib::codec::MessageBuf; +use spdm_lib::commands::algorithms::request::generate_negotiate_algorithms_request; +use spdm_lib::commands::capabilities::request::generate_capabilities_request_local; +use spdm_lib::commands::version::VersionReqPayload; +use spdm_lib::commands::version::request::generate_get_version; +use spdm_lib::context::SpdmContext; +use spdm_lib::platform::transport::SpdmTransport; +use spdm_lib::protocol::{ + AeadCipherSuite, AlgorithmPriorityTable, BaseAsymAlgo, BaseHashAlgo, CapabilityFlags, + DeviceAlgorithms, DeviceCapabilities, DheNamedGroup, KeySchedule, LocalDeviceAlgorithms, + MeasurementHashAlgo, MeasurementSpecification, MelSpecification, OtherParamSupport, + ReqBaseAsymAlg, SpdmVersion, +}; + +use common::{ + transfer, BufferSender, DemoPeerCertStore, DirectClient, MockCertStore, MockEvidence, + MockHash, MockRng, +}; + +/// Requester EID +const REQUESTER_EID: u8 = 8; + +/// Responder EID +const RESPONDER_EID: u8 = 42; + +/// Supported SPDM versions +static SUPPORTED_VERSIONS: [SpdmVersion; 2] = [SpdmVersion::V12, SpdmVersion::V13]; + +/// Create local device algorithms configuration. +fn create_local_algorithms<'a>() -> LocalDeviceAlgorithms<'a> { + let mut measurement_spec = MeasurementSpecification::default(); + measurement_spec.set_dmtf_measurement_spec(1); + + let mut measurement_hash_algo = MeasurementHashAlgo::default(); + measurement_hash_algo.set_tpm_alg_sha_384(1); + + let mut base_asym_algo = BaseAsymAlgo::default(); + base_asym_algo.set_tpm_alg_ecdsa_ecc_nist_p384(1); + + let mut base_hash_algo = BaseHashAlgo::default(); + base_hash_algo.set_tpm_alg_sha_384(1); + + let device_algorithms = DeviceAlgorithms { + measurement_spec, + other_param_support: OtherParamSupport::default(), + measurement_hash_algo, + base_asym_algo, + base_hash_algo, + mel_specification: MelSpecification::default(), + dhe_group: DheNamedGroup::default(), + aead_cipher_suite: AeadCipherSuite::default(), + req_base_asym_algo: ReqBaseAsymAlg::default(), + key_schedule: KeySchedule::default(), + }; + + let algorithm_priority_table = AlgorithmPriorityTable { + measurement_specification: None, + opaque_data_format: None, + base_asym_algo: None, + base_hash_algo: None, + mel_specification: None, + dhe_group: None, + aead_cipher_suite: None, + req_base_asym_algo: None, + key_schedule: None, + }; + + LocalDeviceAlgorithms { + device_algorithms, + algorithm_priority_table, + } +} + +/// Create requester capabilities. +fn create_requester_capabilities() -> DeviceCapabilities { + let mut flags = CapabilityFlags::default(); + flags.set_cert_cap(1); + flags.set_chal_cap(1); + flags.set_meas_cap(0); + flags.set_chunk_cap(1); + + DeviceCapabilities { + ct_exponent: 0, + flags, + data_transfer_size: 1024, + max_spdm_msg_size: 4096, + include_supported_algorithms: false, + } +} + +/// Create responder capabilities. +fn create_responder_capabilities() -> DeviceCapabilities { + let mut flags = CapabilityFlags::default(); + flags.set_cert_cap(1); + flags.set_chal_cap(1); + flags.set_meas_cap(2); + flags.set_meas_fresh_cap(1); + flags.set_chunk_cap(1); + + DeviceCapabilities { + ct_exponent: 0, + flags, + data_transfer_size: 1024, + max_spdm_msg_size: 4096, + include_supported_algorithms: true, + } +} + +/// Sanity check: verify the MCTP layer works before testing SPDM. +#[test] +fn mctp_sanity_check() { + use mctp::Eid; + use openprot_mctp_api::{MctpListener, MctpReqChannel}; + + let buf_a = RefCell::new(Vec::new()); + let buf_b = RefCell::new(Vec::new()); + + let server_a: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(8), 0, BufferSender { packets: &buf_a })); + let server_b: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(42), 0, BufferSender { packets: &buf_b })); + + let stack_a = Stack::new(DirectClient::new(&server_a)); + let stack_b = Stack::new(DirectClient::new(&server_b)); + + // A registers listener FIRST (so router retains matching messages) + let mut listener = stack_a.listener(0x05, 0).expect("listener"); + + // B sends a request to A + let mut req = stack_b.req(8, 0).expect("req channel alloc"); + req.send(0x05, b"hello SPDM").expect("req send"); + + // Transfer B -> A + transfer(&buf_b, &mut server_a.borrow_mut()); + + // A receives + let mut recv_buf = [0u8; 256]; + let (meta, payload, _resp) = listener.recv(&mut recv_buf).expect("listener recv"); + + assert_eq!(payload, b"hello SPDM"); + assert_eq!(meta.msg_type, 0x05); + eprintln!("MCTP sanity check passed!"); +} + +/// SPDM VCA (Version, Capabilities, Algorithms) roundtrip test. +/// +/// This test exercises the full VCA flow between a requester and responder, +/// using in-memory MCTP transport with no platform dependencies. +#[test] +fn spdm_vca_roundtrip() { + // -- Set up MCTP servers with in-memory transport -- + let buf_req = RefCell::new(Vec::new()); + let buf_resp = RefCell::new(Vec::new()); + + let server_req: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(REQUESTER_EID), 0, BufferSender { packets: &buf_req })); + let server_resp: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(RESPONDER_EID), 0, BufferSender { packets: &buf_resp })); + + // -- Create Stack facades (same API as production) -- + let stack_req = Stack::new(DirectClient::new(&server_req)); + let stack_resp = Stack::new(DirectClient::new(&server_resp)); + + // Set EIDs on stacks + stack_req.set_eid(REQUESTER_EID).expect("set requester EID"); + stack_resp.set_eid(RESPONDER_EID).expect("set responder EID"); + + // -- Create SPDM transports -- + let mut transport_req = MctpSpdmTransport::new_requester(&stack_req, RESPONDER_EID); + let mut transport_resp = MctpSpdmTransport::new_responder(&stack_resp); + + // Initialize transports + transport_req.init_sequence().expect("requester transport init"); + transport_resp.init_sequence().expect("responder transport init"); + + // -- Create mock platform implementations -- + // Requester side + let mut req_cert_store = MockCertStore::new(); + let mut req_hash = MockHash::new(); + let mut req_m1_hash = MockHash::new(); + let mut req_l1_hash = MockHash::new(); + let mut req_rng = MockRng::new(); + let req_evidence = MockEvidence::new(); + let mut req_peer_cert_store = DemoPeerCertStore::new(); + + // Responder side + let mut resp_cert_store = MockCertStore::new(); + let mut resp_hash = MockHash::new(); + let mut resp_m1_hash = MockHash::new(); + let mut resp_l1_hash = MockHash::new(); + let mut resp_rng = MockRng::new(); + let resp_evidence = MockEvidence::new(); + + // -- Create SPDM contexts -- + let mut ctx_req = SpdmContext::new( + &SUPPORTED_VERSIONS, + &mut transport_req, + create_requester_capabilities(), + create_local_algorithms(), + &mut req_cert_store, + Some(&mut req_peer_cert_store), + &mut req_hash, + &mut req_m1_hash, + &mut req_l1_hash, + &mut req_rng, + &req_evidence, + ) + .expect("requester context creation"); + + let mut ctx_resp = SpdmContext::new( + &SUPPORTED_VERSIONS, + &mut transport_resp, + create_responder_capabilities(), + create_local_algorithms(), + &mut resp_cert_store, + None, + &mut resp_hash, + &mut resp_m1_hash, + &mut resp_l1_hash, + &mut resp_rng, + &resp_evidence, + ) + .expect("responder context creation"); + + // -- Message buffers (one per context, reused via reset) -- + let mut req_buf_storage = [0u8; 4096]; + let mut resp_buf_storage = [0u8; 4096]; + let mut req_buf = MessageBuf::new(&mut req_buf_storage); + let mut resp_buf = MessageBuf::new(&mut resp_buf_storage); + + // ══════════════════════════════════════════════════════════════════════ + // Step 1: GET_VERSION → VERSION + // ══════════════════════════════════════════════════════════════════════ + + // Requester: generate and send GET_VERSION + generate_get_version(&mut ctx_req, &mut req_buf, VersionReqPayload::new(0, 0)) + .expect("generate GET_VERSION"); + + ctx_req + .requester_send_request(&mut req_buf, RESPONDER_EID) + .expect("send GET_VERSION"); + + // Transfer: requester → responder + transfer(&buf_req, &mut server_resp.borrow_mut()); + buf_req.borrow_mut().clear(); + + // Responder: process request and send VERSION response + ctx_resp + .responder_process_message(&mut resp_buf) + .expect("responder process GET_VERSION"); + + // Transfer: responder → requester + transfer(&buf_resp, &mut server_req.borrow_mut()); + buf_resp.borrow_mut().clear(); + + // Requester: process VERSION response + req_buf.reset(); + ctx_req + .requester_process_message(&mut req_buf) + .expect("requester process VERSION"); + + // ══════════════════════════════════════════════════════════════════════ + // Step 2: GET_CAPABILITIES → CAPABILITIES + // ══════════════════════════════════════════════════════════════════════ + + // Requester: generate and send GET_CAPABILITIES + req_buf.reset(); + generate_capabilities_request_local(&mut ctx_req, &mut req_buf) + .expect("generate GET_CAPABILITIES"); + ctx_req + .requester_send_request(&mut req_buf, RESPONDER_EID) + .expect("send GET_CAPABILITIES"); + + // Transfer: requester → responder + transfer(&buf_req, &mut server_resp.borrow_mut()); + buf_req.borrow_mut().clear(); + + // Responder: process request and send CAPABILITIES response + resp_buf.reset(); + ctx_resp + .responder_process_message(&mut resp_buf) + .expect("responder process GET_CAPABILITIES"); + + // Transfer: responder → requester + transfer(&buf_resp, &mut server_req.borrow_mut()); + buf_resp.borrow_mut().clear(); + + // Requester: process CAPABILITIES response + req_buf.reset(); + ctx_req + .requester_process_message(&mut req_buf) + .expect("requester process CAPABILITIES"); + + // ══════════════════════════════════════════════════════════════════════ + // Step 3: NEGOTIATE_ALGORITHMS → ALGORITHMS + // ══════════════════════════════════════════════════════════════════════ + + // Requester: generate and send NEGOTIATE_ALGORITHMS + req_buf.reset(); + generate_negotiate_algorithms_request(&mut ctx_req, &mut req_buf, None, None, None, None) + .expect("generate NEGOTIATE_ALGORITHMS"); + ctx_req + .requester_send_request(&mut req_buf, RESPONDER_EID) + .expect("send NEGOTIATE_ALGORITHMS"); + + // Transfer: requester → responder + transfer(&buf_req, &mut server_resp.borrow_mut()); + buf_req.borrow_mut().clear(); + + // Responder: process request and send ALGORITHMS response + resp_buf.reset(); + ctx_resp + .responder_process_message(&mut resp_buf) + .expect("responder process NEGOTIATE_ALGORITHMS"); + + // Transfer: responder → requester + transfer(&buf_resp, &mut server_req.borrow_mut()); + buf_resp.borrow_mut().clear(); + + // Requester: process ALGORITHMS response + req_buf.reset(); + ctx_req + .requester_process_message(&mut req_buf) + .expect("requester process ALGORITHMS"); + + // VCA flow completed successfully! +}